fix(web): use generated account-profile contracts (#36927)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
yyh 2026-06-02 15:28:05 +08:00 committed by GitHub
parent 3cd0da303a
commit dea4e66456
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
34 changed files with 268 additions and 151 deletions

View File

@ -18,7 +18,7 @@ from controllers.common.fields import (
SimpleResultResponse,
VerificationTokenResponse,
)
from controllers.common.schema import register_response_schema_models, register_schema_models
from controllers.common.schema import query_params_from_model, register_response_schema_models, register_schema_models
from controllers.console import console_ns
from controllers.console.auth.error import (
EmailAlreadyInUseError,
@ -48,7 +48,7 @@ from fields.base import ResponseModel
from fields.member_fields import Account as AccountResponse
from graphon.file import helpers as file_helpers
from libs.datetime_utils import naive_utc_now
from libs.helper import EmailStr, extract_remote_ip, timezone, to_timestamp
from libs.helper import EmailStr, dump_response, extract_remote_ip, timezone, to_timestamp
from libs.login import current_account_with_tenant, login_required
from models import AccountIntegrate, InvitationCode
from models.account import AccountStatus, InvitationCodeStatus
@ -329,9 +329,9 @@ class AccountNameApi(Resource):
@console_ns.route("/account/avatar")
class AccountAvatarApi(Resource):
@console_ns.expect(console_ns.models[AccountAvatarQuery.__name__])
@console_ns.doc("get_account_avatar")
@console_ns.doc(description="Get account avatar url")
@console_ns.doc(params=query_params_from_model(AccountAvatarQuery))
@console_ns.response(200, "Success", console_ns.models[AvatarUrlResponse.__name__])
@setup_required
@login_required
@ -342,7 +342,7 @@ class AccountAvatarApi(Resource):
avatar = args.avatar
if avatar.startswith(("http://", "https://")):
return {"avatar_url": avatar}
return dump_response(AvatarUrlResponse, {"avatar_url": avatar})
upload_file = db.session.scalar(select(UploadFile).where(UploadFile.id == avatar).limit(1))
if upload_file is None:
@ -355,7 +355,7 @@ class AccountAvatarApi(Resource):
raise NotFound("Avatar file not found")
avatar_url = file_helpers.get_signed_file_url(upload_file_id=upload_file.id)
return {"avatar_url": avatar_url}
return dump_response(AvatarUrlResponse, {"avatar_url": avatar_url})
@console_ns.expect(console_ns.models[AccountAvatarPayload.__name__])
@setup_required

View File

@ -27,7 +27,7 @@ Get account avatar url
| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ------ |
| payload | body | | Yes | [AccountAvatarQuery](#accountavatarquery) |
| avatar | query | Avatar file ID | Yes | string |
##### Responses

View File

@ -140,3 +140,23 @@ def test_service_document_list_documents_query_params_render(monkeypatch: pytest
for name in ("page", "limit", "keyword", "status"):
assert params[name]["in"] == "query"
def test_console_account_avatar_query_param_renders_as_query(monkeypatch: pytest.MonkeyPatch):
from configs import dify_config
from controllers.console import bp as console_bp
monkeypatch.setattr(dify_config, "SWAGGER_UI_ENABLED", True)
app = Flask(__name__)
app.config["TESTING"] = True
app.config["RESTX_INCLUDE_ALL_MODELS"] = True
app.register_blueprint(console_bp)
payload = app.test_client().get("/console/api/swagger.json").get_json()
operation = payload["paths"]["/account/avatar"]["get"]
params = _parameters_by_name(operation)
assert "payload" not in params
assert params["avatar"]["in"] == "query"
assert params["avatar"]["required"] is True

View File

@ -1,6 +1,7 @@
/* eslint-disable ts/no-explicit-any */
import type { ReactNode } from 'react'
import { act, fireEvent, screen, waitFor } from '@testing-library/react'
import { AccountProfileQueryProvider, createAccountProfileQueryClient } from '@/test/account-profile-query'
import { renderWithNuqs } from '@/test/nuqs-testing'
import { AppModeEnum } from '@/types/app'
import ConversationList from '../list'
@ -234,12 +235,15 @@ const renderConversationList = ({
logs?: any
searchParams?: string
} = {}) => {
const queryClient = createAccountProfileQueryClient({ timezone: 'Asia/Shanghai' })
return renderWithNuqs(
<ConversationList
appDetail={appDetail}
logs={logs}
onRefresh={mockOnRefresh}
/>,
<AccountProfileQueryProvider queryClient={queryClient}>
<ConversationList
appDetail={appDetail}
logs={logs}
onRefresh={mockOnRefresh}
/>
</AccountProfileQueryProvider>,
{ searchParams },
)
}

View File

@ -21,6 +21,7 @@ import { StatusDot } from '@langgenius/dify-ui/status-dot'
import { toast } from '@langgenius/dify-ui/toast'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { RiCloseLine, RiEditFill } from '@remixicon/react'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
@ -40,7 +41,7 @@ import CopyIcon from '@/app/components/base/copy-icon'
import Loading from '@/app/components/base/loading'
import MessageLogModal from '@/app/components/base/message-log-modal'
import { WorkflowContextProvider } from '@/app/components/workflow/context'
import { useAppContext } from '@/context/app-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
import useTimestamp from '@/hooks/use-timestamp'
import { fetchChatMessages, updateLogMessageAnnotations, updateLogMessageFeedbacks } from '@/service/log'
@ -158,7 +159,10 @@ type IDetailPanel = {
function DetailPanel({ detail, onFeedback }: IDetailPanel) {
const MIN_ITEMS_FOR_SCROLL_LOADING = 8
const SCROLL_DEBOUNCE_MS = 200
const { userProfile: { timezone } } = useAppContext()
const { data: timezone } = useQuery({
...userProfileQueryOptions(),
select: data => data.profile.timezone ?? undefined,
})
const { formatTime } = useTimestamp()
const { onClose, appDetail } = useContext(DrawerContext)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, showPromptLogModal, setShowPromptLogModal, currentLogModalActiveTab } = useAppStore(useShallow((state: AppStoreState) => ({

View File

@ -2,6 +2,7 @@
import type { FC } from 'react'
import type { App } from '@/types/app'
import { Pagination } from '@langgenius/dify-ui/pagination'
import { useQuery } from '@tanstack/react-query'
import { useDebounce } from 'ahooks'
import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone'
@ -13,7 +14,7 @@ import { useTranslation } from 'react-i18next'
import EmptyElement from '@/app/components/app/log/empty-element'
import Loading from '@/app/components/base/loading'
import { APP_PAGE_LIMIT } from '@/config'
import { useAppContext } from '@/context/app-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { useWorkflowLogs } from '@/service/use-log'
import Filter, { TIME_PERIOD_MAPPING } from './filter'
import List from './list'
@ -33,7 +34,10 @@ export type QueryParam = {
const Logs: FC<ILogsProps> = ({ appDetail }) => {
const { t } = useTranslation()
const { userProfile: { timezone } } = useAppContext()
const { data: timezone } = useQuery({
...userProfileQueryOptions(),
select: data => data.profile.timezone ?? undefined,
})
const [queryParams, setQueryParams] = useState<QueryParam>({ status: 'all', period: '2' })
const [currPage, setCurrPage] = React.useState<number>(0)
const debouncedQueryParams = useDebounce(queryParams, { wait: 500 })

View File

@ -64,6 +64,13 @@ vi.mock('@/utils/model-config', () => ({
formatBooleanInputs: vi.fn((forms, inputs) => inputs),
}))
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: (timestamp: number) => `formatted-${timestamp}`,
formatDate: (value: string) => `formatted-${value}`,
}),
}))
type ChatHookReturn = ReturnType<typeof useChat>
const mockAppData = {

View File

@ -10,6 +10,13 @@ vi.mock('../context', () => ({
})),
}))
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: (timestamp: number) => `formatted-${timestamp}`,
formatDate: (value: string) => `formatted-${value}`,
}),
}))
describe('Answer Component', () => {
const defaultProps = {
item: {

View File

@ -1,8 +1,9 @@
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { Mock } from 'vitest'
import type { UsagePlanInfo } from '@/app/components/billing/type'
import type { AppContextValue } from '@/context/app-context'
import type { ProviderContextState } from '@/context/provider-context'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common'
import { render, screen } from '@testing-library/react'
import { Plan } from '@/app/components/billing/type'
import { mailToSupport } from '@/app/components/header/utils/util'
@ -59,7 +60,7 @@ const buildProviderContext = (overrides: Partial<ProviderContextState> = {}): Pr
})
const buildAppContext = (overrides: Partial<AppContextValue> = {}): AppContextValue => {
const userProfile: UserProfileResponse = {
const userProfile: GetAccountProfileResponse = {
id: 'user-id',
name: 'Test User',
email: 'user@example.com',

View File

@ -1,5 +1,7 @@
import { fireEvent, render, screen } from '@testing-library/react'
import type { ReactElement } from 'react'
import { fireEvent, render as rtlRender, screen } from '@testing-library/react'
import { describe, expect, it, vi } from 'vitest'
import { createAccountProfileQueryWrapper } from '@/test/account-profile-query'
import WrappedDatePicker from '../date-picker'
type TriggerArgs = {
@ -44,6 +46,11 @@ vi.mock('@/hooks/use-timestamp', () => ({
}),
}))
const render = (ui: ReactElement) => {
const Wrapper = createAccountProfileQueryWrapper()
return rtlRender(ui, { wrapper: Wrapper })
}
describe('WrappedDatePicker', () => {
describe('Rendering', () => {
it('should render without crashing', () => {

View File

@ -4,11 +4,12 @@ import {
RiCalendarLine,
RiCloseCircleFill,
} from '@remixicon/react'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
import { useAppContext } from '@/context/app-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import useTimestamp from '@/hooks/use-timestamp'
type Props = {
@ -24,7 +25,10 @@ const WrappedDatePicker = ({
onChange,
}: Props) => {
const { t } = useTranslation()
const { userProfile: { timezone } } = useAppContext()
const { data: timezone } = useQuery({
...userProfileQueryOptions(),
select: data => data.profile.timezone ?? undefined,
})
const { formatTime: formatTimestamp } = useTimestamp()
const handleDateChange = useCallback((date?: dayjs.Dayjs) => {

View File

@ -51,6 +51,12 @@ vi.mock('@/next/navigation', () => ({
}),
}))
vi.mock('@/hooks/use-timestamp', () => ({
default: () => ({
formatTime: (timestamp: number) => `formatted-${timestamp}`,
}),
}))
describe('MetadataDocument', () => {
const mockDocDetail = {
id: 'doc-1',

View File

@ -1,4 +1,4 @@
import type { UserProfileResponse } from '@/models/common'
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import { ToastHost } from '@langgenius/dify-ui/toast'
import { act, fireEvent, render, screen, waitFor, within } from '@testing-library/react'
import { languages } from '@/i18n-config/language'
@ -9,7 +9,7 @@ import LanguagePage from '../index'
const mockRefresh = vi.fn()
const mockMutateUserProfile = vi.fn()
let mockLocale: string | undefined = 'en-US'
let mockUserProfile: UserProfileResponse
let mockUserProfile: GetAccountProfileResponse
vi.mock('@langgenius/dify-ui/select', async () => {
const React = await import('react')
@ -89,7 +89,7 @@ vi.mock('@/i18n-config', () => ({
const updateUserProfileMock = vi.mocked(updateUserProfile)
const createUserProfile = (overrides: Partial<UserProfileResponse> = {}): UserProfileResponse => ({
const createUserProfile = (overrides: Partial<GetAccountProfileResponse> = {}): GetAccountProfileResponse => ({
id: 'user-id',
name: 'Test User',
email: 'test@example.com',

View File

@ -1,7 +1,9 @@
import type { ReactElement } from 'react'
import type { PluginDetail } from '@/app/components/plugins/types'
import { fireEvent, render, screen } from '@testing-library/react'
import { fireEvent, render as rtlRender, screen } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PluginCategoryEnum, PluginSource } from '@/app/components/plugins/types'
import { createAccountProfileQueryWrapper } from '@/test/account-profile-query'
import DetailHeader from '../index'
const mockSetTargetVersion = vi.fn()
@ -10,6 +12,11 @@ const mockHandleUpdate = vi.fn()
const mockHandleUpdatedFromMarketplace = vi.fn()
const mockHandleDelete = vi.fn()
const render = (ui: ReactElement) => {
const Wrapper = createAccountProfileQueryWrapper({ timezone: 'UTC' })
return rtlRender(ui, { wrapper: Wrapper })
}
vi.mock('@/context/app-context', () => ({
useAppContext: () => ({
userProfile: { timezone: 'UTC' },

View File

@ -4,6 +4,7 @@ import type { PluginDetail } from '../../types'
import { Button } from '@langgenius/dify-ui/button'
import { cn } from '@langgenius/dify-ui/cn'
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
import { useQuery } from '@tanstack/react-query'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
@ -12,8 +13,8 @@ import { AuthCategory, PluginAuth } from '@/app/components/plugins/plugin-auth'
import OperationDropdown from '@/app/components/plugins/plugin-detail-panel/operation-dropdown'
import PluginVersionPicker from '@/app/components/plugins/update-plugin/plugin-version-picker'
import { API_PREFIX } from '@/config'
import { useAppContext } from '@/context/app-context'
import { useGetLanguage, useLocale } from '@/context/i18n'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import useTheme from '@/hooks/use-theme'
import { useAllToolProviders } from '@/service/use-tools'
import { getMarketplaceUrl } from '@/utils/var'
@ -72,7 +73,10 @@ const DetailHeader = ({
onUpdate,
}: Props) => {
const { t } = useTranslation()
const { userProfile: { timezone } } = useAppContext()
const { data: timezone } = useQuery({
...userProfileQueryOptions(),
select: data => data.profile.timezone ?? undefined,
})
const { theme } = useTheme()
const locale = useGetLanguage()
const currentLocale = useLocale()

View File

@ -1,13 +1,14 @@
import type { AutoUpdateConfig } from '../types'
import type { PluginDeclaration, PluginDetail } from '@/app/components/plugins/types'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen } from '@testing-library/react'
import { QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render as rtlRender, screen } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import * as React from 'react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createAccountProfileQueryClient } from '@/test/account-profile-query'
import { PluginCategoryEnum, PluginSource } from '../../../types'
import { defaultValue } from '../config'
import AutoUpdateSetting from '../index'
@ -291,21 +292,22 @@ const createMockAutoUpdateConfig = (overrides: Partial<AutoUpdateConfig> = {}):
// Helper Functions
// ================================
const createQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
const createQueryClient = () => createAccountProfileQueryClient({ timezone: mockTimezone })
const render = (ui: React.ReactElement) => {
const queryClient = createQueryClient()
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
return rtlRender(ui, { wrapper: Wrapper })
}
const renderWithQueryClient = (ui: React.ReactElement) => {
const queryClient = createQueryClient()
return render(
<QueryClientProvider client={queryClient}>
{ui}
</QueryClientProvider>,
const Wrapper = ({ children }: { children: React.ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
return rtlRender(ui, { wrapper: Wrapper })
}
// ================================

View File

@ -4,6 +4,7 @@ import type { AutoUpdateConfig } from './types'
import type { TriggerParams } from '@/app/components/base/date-and-time-picker/types'
import { cn } from '@langgenius/dify-ui/cn'
import { RiTimeLine } from '@remixicon/react'
import { useQuery } from '@tanstack/react-query'
import * as React from 'react'
import { useCallback, useMemo } from 'react'
import { Trans, useTranslation } from 'react-i18next'
@ -11,8 +12,8 @@ import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
import { convertTimezoneToOffsetStr } from '@/app/components/base/date-and-time-picker/utils/dayjs'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
import { useAppContext } from '@/context/app-context'
import { useModalContextSelector } from '@/context/modal-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import Label from '../label'
import PluginsPicker from './plugins-picker'
import StrategyPicker from './strategy-picker'
@ -47,7 +48,10 @@ const AutoUpdateSetting: FC<Props> = ({
onChange,
}) => {
const { t } = useTranslation()
const { userProfile: { timezone } } = useAppContext()
const { data: timezone } = useQuery({
...userProfileQueryOptions(),
select: data => data.profile.timezone ?? undefined,
})
const {
strategy_setting,

View File

@ -12,6 +12,7 @@ import {
DatasetPermission,
DataSourceType,
} from '@/models/datasets'
import { AccountProfileQueryProvider, createAccountProfileQueryClient } from '@/test/account-profile-query'
import { RETRIEVE_METHOD, RETRIEVE_TYPE } from '@/types/app'
import { DatasetsDetailContext } from '../../../datasets-detail-store/provider'
import { createDatasetsDetailStore } from '../../../datasets-detail-store/store'
@ -583,25 +584,28 @@ describe('knowledge-retrieval path', () => {
const onDateChange = vi.fn()
const onRemoveCondition = vi.fn()
const onUpdateCondition = vi.fn()
const queryClient = createAccountProfileQueryClient({ timezone: 'UTC' })
const { container } = render(
<div>
<ConditionOperator
variableType={MetadataFilteringVariableType.string}
value={MetadataComparisonOperator.contains}
onSelect={onSelect}
/>
<ConditionDate
value={1710000000}
onChange={onDateChange}
/>
<ConditionItem
metadataList={[createMetadata()]}
condition={createCondition()}
onRemoveCondition={onRemoveCondition}
onUpdateCondition={onUpdateCondition}
/>
</div>,
<AccountProfileQueryProvider queryClient={queryClient}>
<div>
<ConditionOperator
variableType={MetadataFilteringVariableType.string}
value={MetadataComparisonOperator.contains}
onSelect={onSelect}
/>
<ConditionDate
value={1710000000}
onChange={onDateChange}
/>
<ConditionItem
metadataList={[createMetadata()]}
condition={createCondition()}
onRemoveCondition={onRemoveCondition}
onUpdateCondition={onUpdateCondition}
/>
</div>
</AccountProfileQueryProvider>,
)
await user.click(screen.getAllByRole('button', { name: /contains/i })[0]!)

View File

@ -4,11 +4,12 @@ import {
RiCalendarLine,
RiCloseCircleFill,
} from '@remixicon/react'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import DatePicker from '@/app/components/base/date-and-time-picker/date-picker'
import { useAppContext } from '@/context/app-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
type ConditionDateProps = {
value?: number
@ -19,7 +20,10 @@ const ConditionDate = ({
onChange,
}: ConditionDateProps) => {
const { t } = useTranslation()
const { userProfile: { timezone } } = useAppContext()
const { data: timezone } = useQuery({
...userProfileQueryOptions(),
select: data => data.profile.timezone ?? undefined,
})
const handleDateChange = useCallback((date?: dayjs.Dayjs) => {
if (date)

View File

@ -3,6 +3,7 @@ import { renderHook } from '@testing-library/react'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { useAppContext } from '@/context/app-context'
import { createAccountProfileQueryWrapper } from '@/test/account-profile-query'
import { BlockEnum } from '../../../types'
import useConfig from '../use-config'
@ -60,7 +61,7 @@ describe('trigger-schedule/use-config', () => {
frequency: undefined,
timezone: undefined,
visual_config: undefined,
})))
})), { wrapper: createAccountProfileQueryWrapper() })
expect(mockUseNodeCrud).toHaveBeenCalledWith('schedule-node', expect.objectContaining({
mode: 'visual',
@ -78,7 +79,7 @@ describe('trigger-schedule/use-config', () => {
it('updates visual mode configuration and clears cron expression when needed', () => {
const { result } = renderHook(() => useConfig('schedule-node', createData({
cron_expression: '0 0 * * *',
})))
})), { wrapper: createAccountProfileQueryWrapper() })
result.current.handleModeChange('cron')
result.current.handleFrequencyChange('hourly')
@ -107,7 +108,7 @@ describe('trigger-schedule/use-config', () => {
})
it('switches to raw cron mode and clears visual schedule fields', () => {
const { result } = renderHook(() => useConfig('schedule-node', createData()))
const { result } = renderHook(() => useConfig('schedule-node', createData()), { wrapper: createAccountProfileQueryWrapper() })
result.current.handleCronExpressionChange('*/15 * * * *')

View File

@ -1,27 +1,31 @@
import type { ScheduleFrequency, ScheduleMode, ScheduleTriggerNodeType } from './types'
import { useQuery } from '@tanstack/react-query'
import { useCallback, useMemo } from 'react'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { useAppContext } from '@/context/app-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { getDefaultVisualConfig } from './constants'
const useConfig = (id: string, payload: ScheduleTriggerNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { userProfile } = useAppContext()
const { data: timezone } = useQuery({
...userProfileQueryOptions(),
select: data => data.profile.timezone ?? undefined,
})
const frontendPayload = useMemo(() => {
return {
...payload,
mode: payload.mode || 'visual',
frequency: payload.frequency || 'daily',
timezone: payload.timezone || userProfile.timezone || 'UTC',
timezone: payload.timezone || timezone || 'UTC',
visual_config: {
...getDefaultVisualConfig(),
...payload.visual_config,
},
}
}, [payload, userProfile.timezone])
}, [payload, timezone])
const { inputs, setInputs } = useNodeCrud<ScheduleTriggerNodeType>(id, frontendPayload)

View File

@ -1,4 +1,5 @@
import type { SearchParams } from './types'
import { useQuery } from '@tanstack/react-query'
import { useDebounceFn } from 'ahooks'
import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone'
@ -9,9 +10,9 @@ import {
useState,
} from 'react'
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
import { useAppContext } from '@/context/app-context'
import { useModalContextSelector } from '@/context/modal-context'
import { useProviderContext } from '@/context/provider-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
import { useLocalStorage } from '@/hooks/use-local-storage'
import { useRouter, useSearchParams } from '@/next/navigation'
import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education'
@ -81,7 +82,10 @@ const isExpired = (expireAt?: number, timezone?: string) => {
const useEducationReverifyNotice = ({
onNotice,
}: useEducationReverifyNoticeParams) => {
const { userProfile: { timezone } } = useAppContext()
const { data: timezone } = useQuery({
...userProfileQueryOptions(),
select: data => data.profile.timezone ?? undefined,
})
// const [educationInfo, setEducationInfo] = useState<{ is_student: boolean, allow_refresh: boolean, expire_at: number | null } | null>(null)
// const isLoading = !educationInfo
const { educationAccountExpireAt, allowRefreshEducationVerify, isLoadingEducationAccountInfo: isLoading } = useProviderContext()

View File

@ -1,8 +1,9 @@
'use client'
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { PostWorkspacesCurrentResponse } from '@dify/contracts/api/console/workspaces/types.gen'
import type { FC, ReactNode } from 'react'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common'
import { useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
import { useCallback, useEffect, useMemo } from 'react'
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
@ -72,7 +73,7 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
!systemFeatures.branding.enabled,
)
const userProfile = useMemo<UserProfileResponse>(() => userProfileResp?.profile || userProfilePlaceholder, [userProfileResp?.profile])
const userProfile = useMemo<GetAccountProfileResponse>(() => userProfileResp?.profile || userProfilePlaceholder, [userProfileResp?.profile])
const currentWorkspace = useMemo<ICurrentWorkspace>(() => normalizeCurrentWorkspace(currentWorkspaceResp), [currentWorkspaceResp])
const langGeniusVersionInfo = useMemo<LangGeniusVersionResponse>(() => {
if (!userProfileResp?.meta?.currentVersion || !langGeniusVersionQuery.data)

View File

@ -1,11 +1,12 @@
'use client'
import type { ICurrentWorkspace, LangGeniusVersionResponse, UserProfileResponse } from '@/models/common'
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { ICurrentWorkspace, LangGeniusVersionResponse } from '@/models/common'
import { noop } from 'es-toolkit/function'
import { createContext, useContext, useContextSelector } from 'use-context-selector'
export type AppContextValue = {
userProfile: UserProfileResponse
userProfile: GetAccountProfileResponse
mutateUserProfile: VoidFunction
currentWorkspace: ICurrentWorkspace
isCurrentWorkspaceManager: boolean

View File

@ -1,37 +0,0 @@
import { type } from '@orpc/contract'
import { base } from '../base'
export type AccountProfileResponse = {
id: string
name: string
email: string
avatar: string
avatar_url: string | null
is_password_set: boolean
interface_language?: string
interface_theme?: string
timezone?: string
last_login_at?: string
last_active_at?: string
last_login_ip?: string
created_at?: string
}
export const accountProfileContract = base
.route({
path: '/account/profile',
method: 'GET',
})
.output(type<AccountProfileResponse>())
export const accountAvatarContract = base
.route({
path: '/account/avatar',
method: 'GET',
})
.input(type<{
query: {
avatar: string
}
}>())
.output(type<{ avatar_url: string }>())

View File

@ -1,7 +1,6 @@
import type { InferContractRouterInputs } from '@orpc/contract'
import { contract as communityContract } from '@dify/contracts/api/console/orpc.gen'
import { contract as enterpriseContract } from '@dify/contracts/enterprise/orpc.gen'
import { accountAvatarContract, accountProfileContract } from './console/account'
import { appDeleteContract, appListContract, workflowOnlineUsersContract } from './console/apps'
import { bindPartnerStackContract, invoicesContract } from './console/billing'
import {
@ -71,14 +70,6 @@ export type MarketPlaceInputs = InferContractRouterInputs<typeof marketplaceRout
export const consoleRouterContract = {
enterprise: enterpriseContract,
...communityContract,
account: {
...communityContract.account,
avatar: accountAvatarContract,
profile: {
...communityContract.account.profile,
get: accountProfileContract,
},
},
apps: {
...communityContract.apps,
list: appListContract,

View File

@ -1,4 +1,4 @@
import type { AccountProfileResponse } from '@/contract/console/account'
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import { QueryClient } from '@tanstack/react-query'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { resolveServerConsoleApiUrl } from '@/service/server'
@ -18,7 +18,7 @@ vi.mock('@/next/headers', () => ({
cookies: () => cookiesMock(),
}))
const createProfile = (overrides: Partial<AccountProfileResponse> = {}): AccountProfileResponse => ({
const createProfile = (overrides: Partial<GetAccountProfileResponse> = {}): GetAccountProfileResponse => ({
id: 'account-id',
name: 'Dify User',
email: 'user@example.com',

View File

@ -1,4 +1,4 @@
import type { AccountProfileResponse } from '@/contract/console/account'
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import { queryOptions } from '@tanstack/react-query'
import { IS_DEV } from '@/config'
// eslint-disable-next-line no-restricted-imports
@ -6,7 +6,7 @@ import { get } from '@/service/base'
import { consoleQuery } from '@/service/client'
export type UserProfileWithMeta = {
profile: AccountProfileResponse
profile: GetAccountProfileResponse
meta: {
currentVersion: string | null
currentEnv: string | null
@ -24,7 +24,7 @@ export const userProfileQueryOptions = () =>
needAllResponseContent: true,
silent: true,
})
const profile: AccountProfileResponse = await response.clone().json()
const profile: GetAccountProfileResponse = await response.clone().json()
return {
profile,
meta: {

View File

@ -1,5 +1,5 @@
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { UserProfileWithMeta } from './client'
import type { AccountProfileResponse } from '@/contract/console/account'
import { queryOptions } from '@tanstack/react-query'
import { getServerConsoleRequestHeaders, resolveServerConsoleApiUrl, serverConsoleQuery } from '@/service/server'
@ -22,7 +22,7 @@ export const serverUserProfileQueryOptions = () =>
if (!response.ok)
throw response
const profile: AccountProfileResponse = await response.clone().json()
const profile: GetAccountProfileResponse = await response.clone().json()
return {
profile,
meta: {

View File

@ -1,4 +1,5 @@
import { renderHook } from '@testing-library/react'
import { createAccountProfileQueryWrapper } from '@/test/account-profile-query'
import useTimestamp from './use-timestamp'
vi.mock('@/context/app-context', () => ({
@ -23,7 +24,7 @@ vi.mock('@/context/app-context', () => ({
describe('useTimestamp', () => {
describe('formatTime', () => {
it('should format unix timestamp correctly', () => {
const { result } = renderHook(() => useTimestamp())
const { result } = renderHook(() => useTimestamp(), { wrapper: createAccountProfileQueryWrapper() })
const timestamp = 1704132000
expect(result.current.formatTime(timestamp, 'YYYY-MM-DD HH:mm:ss'))
@ -31,7 +32,7 @@ describe('useTimestamp', () => {
})
it('should format with different patterns', () => {
const { result } = renderHook(() => useTimestamp())
const { result } = renderHook(() => useTimestamp(), { wrapper: createAccountProfileQueryWrapper() })
const timestamp = 1704132000
expect(result.current.formatTime(timestamp, 'MM/DD/YYYY'))
@ -44,7 +45,7 @@ describe('useTimestamp', () => {
describe('formatDate', () => {
it('should format date string correctly', () => {
const { result } = renderHook(() => useTimestamp())
const { result } = renderHook(() => useTimestamp(), { wrapper: createAccountProfileQueryWrapper() })
const dateString = '2024-01-01T12:00:00Z'
expect(result.current.formatDate(dateString, 'YYYY-MM-DD HH:mm:ss'))
@ -52,7 +53,7 @@ describe('useTimestamp', () => {
})
it('should format with different patterns', () => {
const { result } = renderHook(() => useTimestamp())
const { result } = renderHook(() => useTimestamp(), { wrapper: createAccountProfileQueryWrapper() })
const dateString = '2024-01-01T12:00:00Z'
expect(result.current.formatDate(dateString, 'MM/DD/YYYY'))

View File

@ -1,15 +1,19 @@
'use client'
import { useQuery } from '@tanstack/react-query'
import dayjs from 'dayjs'
import timezone from 'dayjs/plugin/timezone'
import utc from 'dayjs/plugin/utc'
import { useCallback } from 'react'
import { useAppContext } from '@/context/app-context'
import { userProfileQueryOptions } from '@/features/account-profile/client'
dayjs.extend(utc)
dayjs.extend(timezone)
const useTimestamp = () => {
const { userProfile: { timezone } } = useAppContext()
const { data: timezone } = useQuery({
...userProfileQueryOptions(),
select: data => data.profile.timezone ?? undefined,
})
const formatTime = useCallback((value: number, format: string) => {
return dayjs.unix(value).tz(timezone).format(format)

View File

@ -1,3 +1,4 @@
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { I18nText } from '@/i18n-config/language'
import type { Model } from '@/types/app'
@ -18,24 +19,8 @@ export type InitValidateStatusResponse = {
status: 'finished' | 'not_started'
}
export type UserProfileResponse = {
id: string
name: string
email: string
avatar: string
avatar_url: string | null
is_password_set: boolean
interface_language?: string
interface_theme?: string
timezone?: string
last_login_at?: string
last_active_at?: string
last_login_ip?: string
created_at?: string
}
export type UserProfileOriginResponse = {
json: () => Promise<UserProfileResponse>
json: () => Promise<GetAccountProfileResponse>
bodyUsed: boolean
headers: any
}
@ -50,8 +35,11 @@ export type LangGeniusVersionResponse = {
current_env: string
}
export type Member = Pick<UserProfileResponse, 'id' | 'name' | 'email' | 'last_login_at' | 'last_active_at' | 'created_at' | 'avatar_url'> & {
export type Member = Pick<GetAccountProfileResponse, 'id' | 'name' | 'email' | 'avatar_url'> & {
avatar: string
last_login_at?: string
last_active_at?: string
created_at?: string
status: 'pending' | 'active' | 'banned' | 'closed'
role: 'owner' | 'admin' | 'editor' | 'normal' | 'dataset_operator'
}

View File

@ -371,5 +371,5 @@ export const checkEmailExisted = (body: { email: string }): Promise<CommonRespon
export const getAvatar = async ({ avatar }: { avatar: string }): Promise<{ avatar_url: string }> => {
const { consoleClient } = await import('./client')
return consoleClient.account.avatar({ query: { avatar } })
return consoleClient.account.avatar.get({ query: { avatar } })
}

View File

@ -0,0 +1,70 @@
import type { GetAccountProfileResponse } from '@dify/contracts/api/console/account/types.gen'
import type { QueryClient } from '@tanstack/react-query'
import type { ReactNode } from 'react'
import type { UserProfileWithMeta } from '@/features/account-profile/client'
import { QueryClientProvider, QueryClient as TanStackQueryClient } from '@tanstack/react-query'
import { createElement } from 'react'
import { userProfileQueryOptions } from '@/features/account-profile/client'
const createMockAccountProfile = (
overrides: Partial<GetAccountProfileResponse> = {},
): GetAccountProfileResponse => ({
id: 'user-1',
name: 'Test User',
email: 'test@dify.ai',
avatar: '',
avatar_url: null,
is_password_set: false,
timezone: 'Asia/Shanghai',
...overrides,
})
const createMockUserProfileResponse = (
profile: Partial<GetAccountProfileResponse> = {},
): UserProfileWithMeta => ({
profile: createMockAccountProfile(profile),
meta: {
currentVersion: null,
currentEnv: null,
},
})
export const createAccountProfileQueryClient = (
profile: Partial<GetAccountProfileResponse> = {},
) => {
const queryClient = new TanStackQueryClient({
defaultOptions: {
queries: {
retry: false,
staleTime: Number.POSITIVE_INFINITY,
},
},
})
queryClient.setQueryData(
userProfileQueryOptions().queryKey,
createMockUserProfileResponse(profile),
)
return queryClient
}
export const createAccountProfileQueryWrapper = (
profile: Partial<GetAccountProfileResponse> = {},
) => {
const queryClient = createAccountProfileQueryClient(profile)
return function AccountProfileQueryWrapper({ children }: { children: ReactNode }) {
return createElement(QueryClientProvider, { client: queryClient }, children)
}
}
export function AccountProfileQueryProvider({
children,
queryClient,
}: {
children: ReactNode
queryClient: QueryClient
}) {
return createElement(QueryClientProvider, { client: queryClient }, children)
}