mirror of
https://github.com/langgenius/dify.git
synced 2026-07-29 16:29:36 +08:00
refactor(web): consolidate settings state in URL (#39740)
This commit is contained in:
parent
8629b91517
commit
d4eab4ae88
@ -4,11 +4,10 @@ import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import NotionPageSelector from '@/app/components/base/notion-page-selector/base'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
|
||||
|
||||
const mockInvalidPreImportNotionPages = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
const mockUsePreImportNotionPages = vi.fn()
|
||||
|
||||
vi.mock('@tanstack/react-virtual', () => ({
|
||||
@ -29,16 +28,10 @@ vi.mock('@/service/knowledge/use-import', () => ({
|
||||
useInvalidPreImportNotionPages: () => mockInvalidPreImportNotionPages,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
useModalContextSelector: (
|
||||
selector: (state: {
|
||||
setShowAccountSettingModal: typeof mockSetShowAccountSettingModal
|
||||
}) => unknown,
|
||||
) => selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal }),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
const buildCredential = (
|
||||
id: string,
|
||||
@ -200,8 +193,6 @@ describe('Base Notion Page Selector Flow', () => {
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'common.dataSource.notion.selector.configure' }),
|
||||
)
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
})
|
||||
})
|
||||
|
||||
@ -41,7 +41,6 @@ const render = (ui: ReactElement, options: RenderOptions = {}) => {
|
||||
}
|
||||
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => mockProviderCtx,
|
||||
@ -64,10 +63,6 @@ vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
}),
|
||||
useModalContextSelector: (selector: (s: Record<string, unknown>) => unknown) =>
|
||||
selector({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
|
||||
@ -42,10 +42,8 @@ const render = (ui: ReactElement, options: RenderOptions = {}) => {
|
||||
|
||||
// ─── Mock state ──────────────────────────────────────────────────────────────
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockRouterPush = vi.fn()
|
||||
const mockMutateAsync = vi.fn()
|
||||
const mockSetEducationVerifying = vi.hoisted(() => vi.fn())
|
||||
|
||||
// ─── Context mocks ───────────────────────────────────────────────────────────
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
@ -69,10 +67,6 @@ vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
}),
|
||||
useModalContextSelector: (selector: (s: Record<string, unknown>) => unknown) =>
|
||||
selector({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
// ─── Service mocks ───────────────────────────────────────────────────────────
|
||||
@ -102,10 +96,6 @@ vi.mock('@/hooks/use-async-window-open', () => ({
|
||||
useAsyncWindowOpen: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/education-apply/storage', () => ({
|
||||
useSetEducationVerifying: () => mockSetEducationVerifying,
|
||||
}))
|
||||
|
||||
// ─── External component mocks ───────────────────────────────────────────────
|
||||
vi.mock('@/app/education-apply/verify-state-modal', () => ({
|
||||
default: ({
|
||||
@ -249,20 +239,6 @@ describe('Education Verification Flow', () => {
|
||||
expect(mockRouterPush).toHaveBeenCalledWith('/education-apply?token=edu-token-123')
|
||||
})
|
||||
})
|
||||
|
||||
it('should clear education verifying flag on success', async () => {
|
||||
mockMutateAsync.mockResolvedValue({ token: 'token-xyz' })
|
||||
setupContexts({}, { enableEducationPlan: true, isEducationAccount: false })
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<PlanComp loc="test" />)
|
||||
|
||||
await user.click(screen.getByText(/toVerified/i))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockSetEducationVerifying).toHaveBeenCalledWith(null)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ─── 3. Failed Verification Flow ────────────────────────────────────────
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { SettingsModal } from '@/app/components/header/account-setting/settings-modal'
|
||||
import dynamic from '@/next/dynamic'
|
||||
|
||||
const InSiteMessageNotification = dynamic(
|
||||
@ -25,6 +26,7 @@ export function CommonLayoutGlobalMounts() {
|
||||
<ReadmePanel />
|
||||
<GotoAnything />
|
||||
<WorkflowGeneratorMount />
|
||||
<SettingsModal />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { EducationVerifyActionRecorder } from '@/app/components/education-verify-action-recorder'
|
||||
import { OAuthRegistrationAnalytics } from '@/app/components/oauth-registration-analytics'
|
||||
import { EventEmitterContextProvider } from '@/context/event-emitter-provider'
|
||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||
@ -12,7 +11,6 @@ export async function ConsoleRuntimeProviders({ children }: { children: ReactNod
|
||||
return (
|
||||
<>
|
||||
<OAuthRegistrationAnalytics />
|
||||
<EducationVerifyActionRecorder />
|
||||
<CommonLayoutHydrationBoundary>
|
||||
<ProfileBootstrapGate>
|
||||
<ExternalServiceSync />
|
||||
|
||||
@ -1,47 +0,0 @@
|
||||
import { render, waitFor } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION } from '@/app/education-apply/constants'
|
||||
import { useSearchParams } from '@/next/navigation'
|
||||
import { EducationVerifyActionRecorder } from '../education-verify-action-recorder'
|
||||
|
||||
const setEducationVerifyingMock = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/education-apply/storage', () => ({
|
||||
useSetEducationVerifying: () => setEducationVerifyingMock,
|
||||
}))
|
||||
|
||||
const mockUseSearchParams = vi.mocked(useSearchParams)
|
||||
|
||||
describe('EducationVerifyActionRecorder', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
window.localStorage.clear()
|
||||
mockUseSearchParams.mockReturnValue(
|
||||
new URLSearchParams() as unknown as ReturnType<typeof useSearchParams>,
|
||||
)
|
||||
})
|
||||
|
||||
it('should store the education verification flag when the callback action is present', async () => {
|
||||
mockUseSearchParams.mockReturnValue(
|
||||
new URLSearchParams(
|
||||
`action=${EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION}`,
|
||||
) as unknown as ReturnType<typeof useSearchParams>,
|
||||
)
|
||||
|
||||
render(<EducationVerifyActionRecorder />)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(setEducationVerifyingMock).toHaveBeenCalledWith('yes')
|
||||
})
|
||||
})
|
||||
|
||||
it('should leave localStorage unchanged for unrelated routes', () => {
|
||||
render(<EducationVerifyActionRecorder />)
|
||||
|
||||
expect(setEducationVerifyingMock).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -5,7 +5,6 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import {
|
||||
@ -50,7 +49,7 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
}))
|
||||
const mockOnCancel = vi.fn()
|
||||
const mockOnSave = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
|
||||
const mockUseModelList = vi.fn()
|
||||
const mockUseModelListAndDefaultModel = vi.fn()
|
||||
@ -81,11 +80,10 @@ vi.mock('@/service/use-common', async () => ({
|
||||
useMembers: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink: () => (path: string) => `https://docs${path}`,
|
||||
@ -396,9 +394,7 @@ describe('SettingsModal', () => {
|
||||
)
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -9,6 +9,7 @@ import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { isEqual } from 'es-toolkit/predicate'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Input from '@/app/components/base/input'
|
||||
@ -17,11 +18,13 @@ import { IndexingType } from '@/app/components/datasets/create/step-two'
|
||||
import IndexMethod from '@/app/components/datasets/settings/index-method'
|
||||
import PermissionSelector from '@/app/components/datasets/settings/permission-selector'
|
||||
import { checkShowMultiModalTip } from '@/app/components/datasets/settings/utils'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useModelList } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import ModelSelector from '@/app/components/header/account-setting/model-provider-page/model-selector'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { DatasetPermission } from '@/models/datasets'
|
||||
import { updateDatasetSetting } from '@/service/datasets'
|
||||
@ -56,7 +59,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
const docLink = useDocLink()
|
||||
const ref = useRef(null)
|
||||
const isExternal = currentDataset.provider === 'external'
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [localeCurrentDataset, setLocaleCurrentDataset] = useState({ ...currentDataset })
|
||||
const [topK, setTopK] = useState(localeCurrentDataset?.external_retrieval_model.top_k ?? 2)
|
||||
@ -315,7 +318,7 @@ const SettingsModal: FC<SettingsModalProps> = ({
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 text-left text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
onClick={() => setSettingsDestination('provider')}
|
||||
>
|
||||
{t(($) => $['form.embeddingModelTipLink'], { ns: 'datasetSettings' })}
|
||||
</button>
|
||||
|
||||
@ -6,7 +6,7 @@ import { AppModeEnum, ModelModeType } from '@/types/app'
|
||||
import { AppACLPermission } from '@/utils/permission'
|
||||
import { useConfiguration } from '../use-configuration'
|
||||
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
const mockSetShowAppConfigureFeaturesModal = vi.fn()
|
||||
const mockSetDetailSidebarMode = vi.fn()
|
||||
const mockHandleMultipleModelConfigsChange = vi.fn()
|
||||
@ -73,11 +73,10 @@ vi.mock('@/context/permission-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
@ -493,7 +492,7 @@ describe('useConfiguration', () => {
|
||||
expect(mockFormattingChangedDispatcher).toHaveBeenCalled()
|
||||
expect(mockHandleMultipleModelConfigsChange).toHaveBeenCalled()
|
||||
expect(mockSetDetailSidebarMode).toHaveBeenCalledWith('collapse')
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
expect(mockSetConversationHistoriesRole).toHaveBeenCalledWith({
|
||||
assistant_prefix: 'bot',
|
||||
user_prefix: 'user',
|
||||
|
||||
@ -29,6 +29,7 @@ import { useBoolean, useGetState } from 'ahooks'
|
||||
import { clone } from 'es-toolkit/object'
|
||||
import { produce } from 'immer'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
@ -39,7 +40,6 @@ import {
|
||||
import useAdvancedPromptConfig from '@/app/components/app/configuration/hooks/use-advanced-prompt-config'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { useSetDetailSidebarMode } from '@/app/components/detail-sidebar/storage'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelTypeEnum,
|
||||
@ -48,7 +48,10 @@ import {
|
||||
useModelListAndDefaultModelAndCurrentProviderAndModel,
|
||||
useTextGenerationCurrentProviderAndModelAndModelList,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import {
|
||||
ANNOTATION_DEFAULT,
|
||||
DATASET_DEFAULT,
|
||||
@ -128,7 +131,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
|
||||
const currentWorkspace = useAtomValue(currentWorkspaceAtom)
|
||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
|
||||
const { appDetail, showAppConfigureFeaturesModal, setShowAppConfigureFeaturesModal } =
|
||||
useAppStore(
|
||||
@ -769,7 +772,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
|
||||
onCloseSelectDataSet: hideSelectDataSet,
|
||||
onCompletionParamsChange: setCompletionParams,
|
||||
onConfirmUseGPT4: () => {
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
setSettingsDestination('provider')
|
||||
setShowUseGPT4Confirm(false)
|
||||
},
|
||||
onEnableMultipleModelDebug: handleDebugWithMultipleModelChange,
|
||||
@ -777,7 +780,7 @@ export const useConfiguration = (): ConfigurationViewModel => {
|
||||
onHideDebugPanel: hideDebugPanel,
|
||||
onModelChange: setModel,
|
||||
onMultipleModelConfigsChange: handleMultipleModelConfigsChange,
|
||||
onOpenAccountSettings: () => openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER }),
|
||||
onOpenAccountSettings: () => setSettingsDestination('provider'),
|
||||
onOpenDebugPanel: showDebugPanel,
|
||||
onSaveHistory: (data) => {
|
||||
setConversationHistoriesRole(data)
|
||||
|
||||
@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event'
|
||||
import { createMockProviderContextValue } from '@/__mocks__/provider-context'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { render } from '@/test/console/render'
|
||||
@ -26,16 +24,13 @@ vi.mock('@/context/provider-context', async (importOriginal) => {
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/context/modal-context', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/context/modal-context')>()
|
||||
return {
|
||||
...actual,
|
||||
useModalContextSelector: vi.fn(),
|
||||
}
|
||||
const setSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, setSettingsDestination] }
|
||||
})
|
||||
|
||||
const mockUseProviderContext = vi.mocked(useProviderContext)
|
||||
const mockUseModalContextSelector = vi.mocked(useModalContextSelector)
|
||||
|
||||
function mockProviderPlan(planType: Plan) {
|
||||
mockUseProviderContext.mockReturnValue(
|
||||
@ -50,7 +45,6 @@ function mockProviderPlan(planType: Plan) {
|
||||
}
|
||||
|
||||
describe('ArchivedLogsNotice', () => {
|
||||
const setShowAccountSettingModal = vi.fn()
|
||||
const renderNotice = () => {
|
||||
const { wrapper } = createConsoleQueryWrapper({
|
||||
systemFeatures: { deployment_edition: 'CLOUD' },
|
||||
@ -61,11 +55,6 @@ describe('ArchivedLogsNotice', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockProviderPlan(Plan.professional)
|
||||
mockUseModalContextSelector.mockImplementation((selector) =>
|
||||
selector({
|
||||
setShowAccountSettingModal,
|
||||
} as unknown as Parameters<typeof selector>[0]),
|
||||
)
|
||||
})
|
||||
|
||||
it('should show an accessible notice for paid workspace managers', async () => {
|
||||
@ -78,9 +67,7 @@ describe('ArchivedLogsNotice', () => {
|
||||
expect(within(notice).getByText('appLog.archives.notice.description')).toBeInTheDocument()
|
||||
|
||||
await user.click(within(notice).getByRole('button', { name: 'appLog.archives.notice.action' }))
|
||||
expect(setShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES,
|
||||
})
|
||||
expect(setSettingsDestination).toHaveBeenCalledWith('workflow-log-archives')
|
||||
})
|
||||
|
||||
it('should not show notice for sandbox workspaces', () => {
|
||||
|
||||
@ -3,10 +3,13 @@
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
@ -19,9 +22,7 @@ export function ArchivedLogsNotice() {
|
||||
})
|
||||
const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom)
|
||||
const { enableBilling, plan } = useProviderContext()
|
||||
const setShowAccountSettingModal = useModalContextSelector(
|
||||
(state) => state.setShowAccountSettingModal,
|
||||
)
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
|
||||
if (
|
||||
deploymentEdition !== 'CLOUD' ||
|
||||
@ -53,11 +54,7 @@ export function ArchivedLogsNotice() {
|
||||
<Button
|
||||
variant="primary"
|
||||
className="shrink-0"
|
||||
onClick={() =>
|
||||
setShowAccountSettingModal({
|
||||
payload: ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES,
|
||||
})
|
||||
}
|
||||
onClick={() => setSettingsDestination('workflow-log-archives')}
|
||||
>
|
||||
{t(($) => $['archives.notice.action'], { ns: 'appLog' })}
|
||||
</Button>
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { cleanup, screen } from '@testing-library/react'
|
||||
import {
|
||||
clearAllMocks,
|
||||
defaultModalContext,
|
||||
interactions,
|
||||
mockUseModalContext,
|
||||
mockSetSettingsDestination,
|
||||
scenarios,
|
||||
setDeploymentEdition,
|
||||
} from './test-utils'
|
||||
@ -11,15 +10,9 @@ import {
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('APIKeyInfoPanel - Cloud Edition', () => {
|
||||
const setShowAccountSettingModal = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllMocks()
|
||||
setDeploymentEdition('CLOUD')
|
||||
mockUseModalContext.mockReturnValue({
|
||||
...defaultModalContext,
|
||||
setShowAccountSettingModal,
|
||||
})
|
||||
})
|
||||
|
||||
it('hides the panel when an API key already exists', () => {
|
||||
@ -28,9 +21,9 @@ describe('APIKeyInfoPanel - Cloud Edition', () => {
|
||||
})
|
||||
|
||||
it('opens provider settings from the primary action', () => {
|
||||
scenarios.withMockModal(setShowAccountSettingModal)
|
||||
scenarios.withAPIKeyNotSet()
|
||||
interactions.clickMainButton()
|
||||
expect(setShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
})
|
||||
|
||||
it('does not show the self-hosted Cloud link', () => {
|
||||
|
||||
@ -1,9 +1,8 @@
|
||||
import { cleanup, screen } from '@testing-library/react'
|
||||
import {
|
||||
clearAllMocks,
|
||||
defaultModalContext,
|
||||
interactions,
|
||||
mockUseModalContext,
|
||||
mockSetSettingsDestination,
|
||||
scenarios,
|
||||
setDeploymentEdition,
|
||||
textKeys,
|
||||
@ -12,15 +11,9 @@ import {
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('APIKeyInfoPanel - Community Edition', () => {
|
||||
const setShowAccountSettingModal = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
clearAllMocks()
|
||||
setDeploymentEdition('COMMUNITY')
|
||||
mockUseModalContext.mockReturnValue({
|
||||
...defaultModalContext,
|
||||
setShowAccountSettingModal,
|
||||
})
|
||||
})
|
||||
|
||||
it('hides the panel when an API key already exists', () => {
|
||||
@ -29,9 +22,9 @@ describe('APIKeyInfoPanel - Community Edition', () => {
|
||||
})
|
||||
|
||||
it('opens provider settings from the primary action', () => {
|
||||
scenarios.withMockModal(setShowAccountSettingModal)
|
||||
scenarios.withAPIKeyNotSet()
|
||||
interactions.clickMainButton()
|
||||
expect(setShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
})
|
||||
|
||||
it('links self-hosted users to Dify Cloud safely', () => {
|
||||
|
||||
@ -1,20 +1,16 @@
|
||||
import type { DeploymentEdition } from '@dify/contracts/api/console/system-features/types.gen'
|
||||
import type { RenderOptions } from '@testing-library/react'
|
||||
import type { Mock, MockedFunction } from 'vitest'
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import type { MockedFunction } from 'vitest'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import { noop } from 'es-toolkit/function'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import {
|
||||
useModalContext as actualUseModalContext,
|
||||
useModalContextSelector as actualUseModalContextSelector,
|
||||
} from '@/context/modal-context'
|
||||
import { useProviderContext as actualUseProviderContext } from '@/context/provider-context'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import APIKeyInfoPanel from '../index'
|
||||
|
||||
const { mockRouterPush } = vi.hoisted(() => ({
|
||||
const { mockRouterPush, mockSetSettingsDestination } = vi.hoisted(() => ({
|
||||
mockRouterPush: vi.fn(),
|
||||
mockSetSettingsDestination: vi.fn(),
|
||||
}))
|
||||
|
||||
// Mock the modules before importing the functions
|
||||
@ -22,10 +18,13 @@ vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
useModalContextSelector: vi.fn(),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return {
|
||||
...actual,
|
||||
useQueryState: () => [null, mockSetSettingsDestination],
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
@ -37,11 +36,6 @@ vi.mock('@/next/navigation', () => ({
|
||||
const mockUseProviderContext = actualUseProviderContext as MockedFunction<
|
||||
typeof actualUseProviderContext
|
||||
>
|
||||
const mockUseModalContext = actualUseModalContext as MockedFunction<typeof actualUseModalContext>
|
||||
const mockUseModalContextSelector = actualUseModalContextSelector as MockedFunction<
|
||||
typeof actualUseModalContextSelector
|
||||
>
|
||||
|
||||
// Default mock data
|
||||
const defaultProviderContext = {
|
||||
modelProviders: [],
|
||||
@ -78,25 +72,8 @@ const defaultProviderContext = {
|
||||
humanInputEmailDeliveryEnabled: false,
|
||||
}
|
||||
|
||||
const defaultModalContext: ModalContextState = {
|
||||
hasBlockingModalOpen: false,
|
||||
setShowAccountSettingModal: noop,
|
||||
setShowModerationSettingModal: noop,
|
||||
setShowExternalDataToolModal: noop,
|
||||
setShowPricingModal: noop,
|
||||
setShowAnnotationFullModal: noop,
|
||||
setShowModelModal: noop,
|
||||
setShowExternalKnowledgeAPIModal: noop,
|
||||
setShowModelLoadBalancingModal: noop,
|
||||
setShowOpeningModal: noop,
|
||||
setShowUpdatePluginModal: noop,
|
||||
setShowEducationExpireNoticeModal: noop,
|
||||
setShowTriggerEventsLimitModal: noop,
|
||||
}
|
||||
|
||||
type MockOverrides = {
|
||||
providerContext?: Partial<typeof defaultProviderContext>
|
||||
modalContext?: Partial<typeof defaultModalContext>
|
||||
}
|
||||
|
||||
type APIKeyInfoPanelRenderOptions = {
|
||||
@ -112,18 +89,6 @@ function setupMocks(overrides: MockOverrides = {}) {
|
||||
...defaultProviderContext,
|
||||
...overrides.providerContext,
|
||||
})
|
||||
|
||||
mockUseModalContext.mockReturnValue({
|
||||
...defaultModalContext,
|
||||
...overrides.modalContext,
|
||||
})
|
||||
|
||||
mockUseModalContextSelector.mockImplementation((selector) =>
|
||||
selector({
|
||||
...defaultModalContext,
|
||||
...overrides.modalContext,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
// Custom render function
|
||||
@ -157,15 +122,6 @@ export const scenarios = {
|
||||
...overrides,
|
||||
},
|
||||
}),
|
||||
|
||||
// Render with mock modal function
|
||||
withMockModal: (mockSetShowAccountSettingModal: Mock, overrides: MockOverrides = {}) =>
|
||||
renderAPIKeyInfoPanel({
|
||||
mockOverrides: {
|
||||
modalContext: { setShowAccountSettingModal: mockSetShowAccountSettingModal },
|
||||
...overrides,
|
||||
},
|
||||
}),
|
||||
}
|
||||
|
||||
// Common user interactions
|
||||
@ -211,4 +167,4 @@ export function setDeploymentEdition(value: DeploymentEdition) {
|
||||
}
|
||||
|
||||
// Export mock functions for external access
|
||||
export { defaultModalContext, mockUseModalContext }
|
||||
export { mockSetSettingsDestination }
|
||||
|
||||
@ -4,12 +4,15 @@ import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
|
||||
@ -21,7 +24,7 @@ const APIKeyInfoPanel: FC = () => {
|
||||
const isCloud = deploymentEdition === 'CLOUD'
|
||||
|
||||
const { isAPIKeySet } = useProviderContext()
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
|
||||
const { t } = useTranslation()
|
||||
|
||||
@ -67,7 +70,7 @@ const APIKeyInfoPanel: FC = () => {
|
||||
<Button
|
||||
variant="primary"
|
||||
className="mt-2 space-x-2"
|
||||
onClick={() => openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
onClick={() => setSettingsDestination('provider')}
|
||||
>
|
||||
<div className="text-sm font-medium">
|
||||
{t(($) => $['apiKeyInfo.setAPIBtn'], { ns: 'appOverview' })}
|
||||
|
||||
@ -59,12 +59,10 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
const mockOnClose = vi.fn()
|
||||
const mockOnSave = vi.fn()
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockUseProviderContext = vi.fn<() => ProviderContextState>()
|
||||
|
||||
const buildModalContext = (): ModalContextState => ({
|
||||
hasBlockingModalOpen: false,
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowModerationSettingModal: vi.fn(),
|
||||
setShowExternalDataToolModal: vi.fn(),
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
@ -135,7 +133,6 @@ describe('SettingsModal', () => {
|
||||
mockOnClose.mockClear()
|
||||
mockOnSave.mockClear()
|
||||
mockSetShowPricingModal.mockClear()
|
||||
mockSetShowAccountSettingModal.mockClear()
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
...baseProviderContextValue,
|
||||
enableBilling: true,
|
||||
@ -423,7 +420,6 @@ describe('SettingsModal', () => {
|
||||
fireEvent.click((await screen.findAllByText('billing.upgradeBtn.encourageShort'))[0]!)
|
||||
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
||||
expect(mockSetShowAccountSettingModal).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should hide the upgrade badge for non-sandbox plans', async () => {
|
||||
|
||||
@ -25,7 +25,9 @@ import dayjs from 'dayjs'
|
||||
import { APP_PAGE_LIMIT } from '@/config'
|
||||
import { WorkflowRunTriggeredFrom } from '@/models/log'
|
||||
import * as useLogModule from '@/service/use-log'
|
||||
import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { render } from '@/test/console/render'
|
||||
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||
import { TIME_PERIOD_MAPPING } from '../filter'
|
||||
import Logs from '../index'
|
||||
|
||||
@ -124,7 +126,15 @@ const mockedUseWorkflowLogs = useLogModule.useWorkflowLogs as MockedFunction<
|
||||
// ============================================================================
|
||||
|
||||
const renderWithQueryClient = (ui: React.ReactElement) => {
|
||||
return renderWithConsoleQuery(ui)
|
||||
const { wrapper: QueryWrapper } = createConsoleQueryWrapper()
|
||||
const { wrapper: NuqsWrapper } = createNuqsTestWrapper()
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<QueryWrapper>
|
||||
<NuqsWrapper>{children}</NuqsWrapper>
|
||||
</QueryWrapper>
|
||||
)
|
||||
|
||||
return render(ui, { wrapper })
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
|
||||
@ -13,12 +13,11 @@ vi.mock('@langgenius/dify-ui/toast', () => ({
|
||||
},
|
||||
}))
|
||||
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
let mockCodeBasedExtensions: { data: { data: Record<string, unknown>[] } } = { data: { data: [] } }
|
||||
let mockModelProvidersData: {
|
||||
@ -52,10 +51,6 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/declaration
|
||||
CustomConfigurationStatusEnum: { active: 'active' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/constants', () => ({
|
||||
ACCOUNT_SETTING_TAB: { PROVIDER: 'provider' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/api-based-extension-page/selector', () => ({
|
||||
ApiBasedExtensionSelector: ({ onChange }: { value: string; onChange: (v: string) => void }) => (
|
||||
<div data-testid="api-selector">
|
||||
@ -668,12 +663,7 @@ describe('ModerationSettingModal', () => {
|
||||
|
||||
fireEvent.click(screen.getByText(/settings\.provider/))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalled()
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'provider',
|
||||
onCancelCallback: expect.any(Function),
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
})
|
||||
|
||||
it('should not save when OpenAI type is selected but not configured', async () => {
|
||||
|
||||
@ -6,13 +6,16 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Dialog, DialogContent } from '@langgenius/dify-ui/dialog'
|
||||
import { Textarea } from '@langgenius/dify-ui/textarea'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import { ApiBasedExtensionSelector } from '@/app/components/header/account-setting/api-based-extension-page/selector'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { CustomConfigurationStatusEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useDocLink, useLocale } from '@/context/i18n'
|
||||
import { LanguagesSupported } from '@/i18n-config/language'
|
||||
import { useCodeBasedExtensions, useModelProviders } from '@/service/use-common'
|
||||
@ -56,14 +59,10 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({ data, onCance
|
||||
const { t } = useTranslation()
|
||||
const docLink = useDocLink()
|
||||
const locale = useLocale()
|
||||
const {
|
||||
data: modelProviders,
|
||||
isPending: isLoading,
|
||||
refetch: refetchModelProviders,
|
||||
} = useModelProviders()
|
||||
const { data: modelProviders, isPending: isLoading } = useModelProviders()
|
||||
const localeDataRef = useRef<ModerationConfig>(data)
|
||||
const [localeData, setLocaleData] = useState<ModerationConfig>(data)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const updateLocaleData = useCallback(
|
||||
(
|
||||
update: ModerationConfig | ((current: ModerationConfig) => ModerationConfig),
|
||||
@ -78,10 +77,7 @@ const ModerationSettingModal: FC<ModerationSettingModalProps> = ({ data, onCance
|
||||
[],
|
||||
)
|
||||
const handleOpenSettingsModal = () => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
onCancelCallback: refetchModelProviders,
|
||||
})
|
||||
setSettingsDestination('provider')
|
||||
}
|
||||
const { data: codeBasedExtensionList } = useCodeBasedExtensions('moderation')
|
||||
const openaiProvider = modelProviders?.data.find(
|
||||
|
||||
@ -3,9 +3,7 @@ import type { DataSourceNotionWorkspace } from '@/models/common'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
|
||||
import { useModalContext, useModalContextSelector } from '@/context/modal-context'
|
||||
import {
|
||||
useInvalidPreImportNotionPages,
|
||||
usePreImportNotionPages,
|
||||
@ -19,10 +17,11 @@ vi.mock('@/service/knowledge/use-import', () => ({
|
||||
useInvalidPreImportNotionPages: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
useModalContextSelector: vi.fn(),
|
||||
}))
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
const buildCredential = (
|
||||
id: string,
|
||||
@ -110,21 +109,10 @@ const createPreImportResult = ({
|
||||
}) as ReturnType<typeof usePreImportNotionPages>
|
||||
|
||||
describe('NotionPageSelector Base', () => {
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockInvalidPreImportNotionPages = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
} as unknown as ReturnType<typeof useModalContext>)
|
||||
vi.mocked(useModalContextSelector).mockImplementation((selector) => {
|
||||
// Execute the selector to get branch/func coverage for the inline function
|
||||
selector({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
} as unknown as Parameters<Parameters<typeof useModalContextSelector>[0]>[0])
|
||||
return mockSetShowAccountSettingModal
|
||||
})
|
||||
vi.mocked(useInvalidPreImportNotionPages).mockReturnValue(mockInvalidPreImportNotionPages)
|
||||
})
|
||||
|
||||
@ -145,9 +133,7 @@ describe('NotionPageSelector Base', () => {
|
||||
const connectButton = screen.getByRole('button', { name: 'datasetCreation.stepOne.connect' })
|
||||
await user.click(connectButton)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
})
|
||||
|
||||
it('should render page selector and allow selecting a page tree', async () => {
|
||||
@ -232,9 +218,7 @@ describe('NotionPageSelector Base', () => {
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: 'common.dataSource.notion.selector.configure' }),
|
||||
)
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
})
|
||||
|
||||
it('should preview a page and call onPreview when callback is provided', async () => {
|
||||
|
||||
@ -5,10 +5,13 @@ import type {
|
||||
DataSourceNotionWorkspace,
|
||||
NotionPage,
|
||||
} from '@/models/common'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import {
|
||||
useInvalidPreImportNotionPages,
|
||||
usePreImportNotionPages,
|
||||
@ -43,7 +46,7 @@ const NotionPageSelector = ({
|
||||
}: NotionPageSelectorProps) => {
|
||||
const { t } = useTranslation()
|
||||
const [searchValue, setSearchValue] = useState('')
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
|
||||
const invalidPreImportNotionPages = useInvalidPreImportNotionPages()
|
||||
|
||||
@ -162,8 +165,8 @@ const NotionPageSelector = ({
|
||||
)
|
||||
|
||||
const handleConfigureNotion = useCallback(() => {
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE })
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('data-source')
|
||||
}, [setSettingsDestination])
|
||||
|
||||
if (isFetchingNotionPagesError) {
|
||||
return <NotionConnector onSetting={handleConfigureNotion} />
|
||||
|
||||
@ -6,18 +6,15 @@ import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useUnmountedRef } from 'ahooks'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import * as React from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ApiAggregate, TriggerAll } from '@/app/components/base/icons/src/vender/workflow'
|
||||
import UsageInfo from '@/app/components/billing/usage-info'
|
||||
import { useSetEducationVerifying } from '@/app/education-apply/storage'
|
||||
import VerifyStateModal from '@/app/education-apply/verify-state-modal'
|
||||
import { userProfileEmailAtom } from '@/context/account-state'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { isCurrentWorkspaceManagerAtom } from '@/context/workspace-state'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { usePathname, useRouter } from '@/next/navigation'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import { useEducationVerify } from '@/service/use-education'
|
||||
import { getDaysUntilEndOfMonth } from '@/utils/time'
|
||||
import Loading from '../../base/icons/src/public/thought/Loading'
|
||||
@ -41,7 +38,6 @@ const PlanComp: FC<Props> = ({ loc }) => {
|
||||
})
|
||||
const isCloudEdition = deploymentEdition === 'CLOUD'
|
||||
const router = useRouter()
|
||||
const path = usePathname()
|
||||
const userProfileEmail = useAtomValue(userProfileEmailAtom)
|
||||
const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom)
|
||||
const { plan, enableEducationPlan, allowRefreshEducationVerify, isEducationAccount } =
|
||||
@ -65,14 +61,11 @@ const PlanComp: FC<Props> = ({ loc }) => {
|
||||
const [showModal, setShowModal] = React.useState(false)
|
||||
const { handleEducationDiscount, isEducationDiscountLoading } = useEducationDiscount()
|
||||
const { mutateAsync, isPending } = useEducationVerify()
|
||||
const setShowAccountSettingModal = useModalContextSelector((s) => s.setShowAccountSettingModal)
|
||||
const setEducationVerifying = useSetEducationVerifying()
|
||||
const unmountedRef = useUnmountedRef()
|
||||
const handleVerify = () => {
|
||||
if (isPending) return
|
||||
mutateAsync()
|
||||
.then((res) => {
|
||||
setEducationVerifying(null)
|
||||
if (unmountedRef.current) return
|
||||
router.push(`/education-apply?token=${res.token}`)
|
||||
})
|
||||
@ -80,10 +73,6 @@ const PlanComp: FC<Props> = ({ loc }) => {
|
||||
setShowModal(true)
|
||||
})
|
||||
}
|
||||
useEffect(() => {
|
||||
// setShowAccountSettingModal would prevent navigation
|
||||
if (path.startsWith('/education-apply')) setShowAccountSettingModal(null)
|
||||
}, [path, setShowAccountSettingModal])
|
||||
return (
|
||||
<div className="relative rounded-2xl border-[0.5px] border-effects-highlight-lightmode-off bg-background-section-burn">
|
||||
<div className="p-6 pb-2">
|
||||
|
||||
@ -79,23 +79,11 @@ vi.mock('@/context/system-features-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
// Mock modal context
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
useModalContextSelector: (
|
||||
selector: (state: {
|
||||
setShowAccountSettingModal: typeof mockSetShowAccountSettingModal
|
||||
}) => unknown,
|
||||
) => {
|
||||
const state = {
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}
|
||||
return selector(state)
|
||||
},
|
||||
}))
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
// Mock dataset detail context
|
||||
let mockDatasetDetail: DataSet | undefined
|
||||
@ -650,7 +638,7 @@ describe('DatasetUpdateForm', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('step-one-setting'))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'data-source' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
})
|
||||
|
||||
it('should open provider settings when onSetting is called from StepTwo', () => {
|
||||
@ -659,7 +647,7 @@ describe('DatasetUpdateForm', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('step-two-setting'))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'provider' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
})
|
||||
|
||||
it('should update crawl options when onCrawlOptionsChange is called', () => {
|
||||
|
||||
@ -9,13 +9,16 @@ import type {
|
||||
import type { RETRIEVE_METHOD } from '@/types/app'
|
||||
import { produce } from 'immer'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { userProfileIdAtom } from '@/context/account-state'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import {
|
||||
@ -51,7 +54,7 @@ const DEFAULT_CRAWL_OPTIONS: CrawlOptions = {
|
||||
const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const datasetDetail = useDatasetDetailContextWithSelector((state) => state.dataset)
|
||||
const currentUserId = useAtomValue(userProfileIdAtom)
|
||||
const isLoadingWorkspacePermissionKeys = useAtomValue(workspacePermissionKeysLoadingAtom)
|
||||
@ -159,9 +162,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||
{step === 1 && (
|
||||
<StepOne
|
||||
authedDataSourceList={dataSourceList?.result || []}
|
||||
onSetting={() =>
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE })
|
||||
}
|
||||
onSetting={() => setSettingsDestination('data-source')}
|
||||
datasetId={datasetId}
|
||||
dataSourceType={dataSourceType}
|
||||
dataSourceTypeDisable={!!datasetDetail?.data_source_type}
|
||||
@ -185,7 +186,7 @@ const DatasetUpdateForm = ({ datasetId }: DatasetUpdateFormProps) => {
|
||||
{step === 2 && (!datasetId || (datasetId && !!datasetDetail)) && (
|
||||
<StepTwo
|
||||
isAPIKeySet={!!embeddingsDefaultModel}
|
||||
onSetting={() => openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })}
|
||||
onSetting={() => setSettingsDestination('provider')}
|
||||
indexingType={datasetDetail?.indexing_technique}
|
||||
datasetId={datasetId}
|
||||
dataSourceType={dataSourceType}
|
||||
|
||||
@ -5,9 +5,9 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { CredentialTypeEnum } from '@/app/components/plugins/plugin-auth/types'
|
||||
import Website from '../index'
|
||||
|
||||
const { mockRouterPush, mockSetShowAccountSettingModal } = vi.hoisted(() => ({
|
||||
const { mockRouterPush, mockSetSettingsDestination } = vi.hoisted(() => ({
|
||||
mockRouterPush: vi.fn(),
|
||||
mockSetShowAccountSettingModal: vi.fn(),
|
||||
mockSetSettingsDestination: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
@ -16,11 +16,10 @@ vi.mock('@/next/navigation', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
vi.mock('../index.module.css', () => ({
|
||||
default: {
|
||||
@ -272,7 +271,7 @@ describe('Website', () => {
|
||||
const configButton = screen.getByTestId('no-data-config-button')
|
||||
fireEvent.click(configButton)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'data-source' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,9 +10,9 @@ import FireCrawl from '../index'
|
||||
// Mock API service
|
||||
const mockCreateFirecrawlTask = vi.fn()
|
||||
const mockCheckFirecrawlTaskStatus = vi.fn()
|
||||
const { mockRouterPush, mockSetShowAccountSettingModal } = vi.hoisted(() => ({
|
||||
const { mockRouterPush, mockSetSettingsDestination } = vi.hoisted(() => ({
|
||||
mockRouterPush: vi.fn(),
|
||||
mockSetShowAccountSettingModal: vi.fn(),
|
||||
mockSetSettingsDestination: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
@ -26,12 +26,10 @@ vi.mock('@/service/datasets', () => ({
|
||||
checkFirecrawlTaskStatus: (...args: unknown[]) => mockCheckFirecrawlTaskStatus(...args),
|
||||
}))
|
||||
|
||||
// Mock modal context
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
// Mock sleep utility to speed up tests
|
||||
vi.mock('@/utils', () => ({
|
||||
@ -178,7 +176,7 @@ describe('FireCrawl', () => {
|
||||
const configButton = screen.getByText(/configureFirecrawl/i)
|
||||
await user.click(configButton)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'data-source' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -2,11 +2,14 @@
|
||||
import type { FC } from 'react'
|
||||
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { checkFirecrawlTaskStatus, createFirecrawlTask } from '@/service/datasets'
|
||||
import { sleep } from '@/utils'
|
||||
import CrawledResult from '../base/crawled-result'
|
||||
@ -69,12 +72,10 @@ const FireCrawl: FC<Props> = ({
|
||||
isMountedRef.current = false
|
||||
}
|
||||
}, [])
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const handleSetting = useCallback(() => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('data-source')
|
||||
}, [setSettingsDestination])
|
||||
const checkValid = useCallback(
|
||||
(url: string) => {
|
||||
let errorMsg = ''
|
||||
|
||||
@ -3,11 +3,14 @@ import type { FC } from 'react'
|
||||
import type { DataSourceAuth } from '@/app/components/header/account-setting/data-source-page-new/types'
|
||||
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import {
|
||||
ENABLE_WEBSITE_FIRECRAWL,
|
||||
ENABLE_WEBSITE_JINAREADER,
|
||||
@ -42,7 +45,7 @@ const Website: FC<Props> = ({
|
||||
authedDataSourceList,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const [selectedProvider, setSelectedProvider] = useState<DataSourceProvider>(
|
||||
DataSourceProvider.jinaReader,
|
||||
)
|
||||
@ -62,10 +65,8 @@ const Website: FC<Props> = ({
|
||||
)
|
||||
|
||||
const handleOnConfig = useCallback(() => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('data-source')
|
||||
}, [setSettingsDestination])
|
||||
|
||||
const source = availableProviders.find((source) => source.provider === selectedProvider)
|
||||
|
||||
|
||||
@ -6,9 +6,9 @@ import { checkJinaReaderTaskStatus, createJinaReaderTask } from '@/service/datas
|
||||
import { sleep } from '@/utils'
|
||||
import JinaReader from '../index'
|
||||
|
||||
const { mockRouterPush, mockSetShowAccountSettingModal } = vi.hoisted(() => ({
|
||||
const { mockRouterPush, mockSetSettingsDestination } = vi.hoisted(() => ({
|
||||
mockRouterPush: vi.fn(),
|
||||
mockSetShowAccountSettingModal: vi.fn(),
|
||||
mockSetSettingsDestination: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
@ -26,12 +26,10 @@ vi.mock('@/utils', () => ({
|
||||
sleep: vi.fn(() => Promise.resolve()),
|
||||
}))
|
||||
|
||||
// Mock modal context
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
// Mock doc link context
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
@ -400,14 +398,14 @@ describe('JinaReader', () => {
|
||||
const configButton = screen.getByText('datasetCreation.stepOne.website.configureJinaReader')
|
||||
fireEvent.click(configButton)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledTimes(1)
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
|
||||
// Rerender and click again
|
||||
rerender(<JinaReader {...props} />)
|
||||
fireEvent.click(configButton)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledTimes(2)
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledTimes(2)
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -446,7 +444,7 @@ describe('JinaReader', () => {
|
||||
const configButton = screen.getByText('datasetCreation.stepOne.website.configureJinaReader')
|
||||
await userEvent.click(configButton)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'data-source' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@ -2,11 +2,14 @@
|
||||
import type { FC } from 'react'
|
||||
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { checkJinaReaderTaskStatus, createJinaReaderTask } from '@/service/datasets'
|
||||
import { sleep } from '@/utils'
|
||||
import CrawledResult from '../base/crawled-result'
|
||||
@ -44,12 +47,10 @@ const JinaReader: FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
const [step, setStep] = useState<Step>(Step.init)
|
||||
const [controlFoldOptions, setControlFoldOptions] = useState<number>(0)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const handleSetting = useCallback(() => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('data-source')
|
||||
}, [setSettingsDestination])
|
||||
const checkValid = useCallback(
|
||||
(url: string) => {
|
||||
let errorMsg = ''
|
||||
|
||||
@ -6,9 +6,9 @@ import { checkWatercrawlTaskStatus, createWatercrawlTask } from '@/service/datas
|
||||
import { sleep } from '@/utils'
|
||||
import WaterCrawl from '../index'
|
||||
|
||||
const { mockRouterPush, mockSetShowAccountSettingModal } = vi.hoisted(() => ({
|
||||
const { mockRouterPush, mockSetSettingsDestination } = vi.hoisted(() => ({
|
||||
mockRouterPush: vi.fn(),
|
||||
mockSetShowAccountSettingModal: vi.fn(),
|
||||
mockSetSettingsDestination: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
@ -26,12 +26,10 @@ vi.mock('@/utils', () => ({
|
||||
sleep: vi.fn(() => Promise.resolve()),
|
||||
}))
|
||||
|
||||
// Mock modal context
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
// Mock i18n context
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
@ -396,14 +394,14 @@ describe('WaterCrawl', () => {
|
||||
const configButton = screen.getByText('datasetCreation.stepOne.website.configureWatercrawl')
|
||||
fireEvent.click(configButton)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledTimes(1)
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
|
||||
// Rerender and click again
|
||||
rerender(<WaterCrawl {...props} />)
|
||||
fireEvent.click(configButton)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledTimes(2)
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledTimes(2)
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@ -448,7 +446,7 @@ describe('WaterCrawl', () => {
|
||||
const configButton = screen.getByText('datasetCreation.stepOne.website.configureWatercrawl')
|
||||
await userEvent.click(configButton)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'data-source' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
expect(mockRouterPush).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
|
||||
@ -2,11 +2,14 @@
|
||||
import type { FC } from 'react'
|
||||
import type { CrawlOptions, CrawlResultItem } from '@/models/datasets'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { checkWatercrawlTaskStatus, createWatercrawlTask } from '@/service/datasets'
|
||||
import { sleep } from '@/utils'
|
||||
import CrawledResult from '../base/crawled-result'
|
||||
@ -49,12 +52,10 @@ const WaterCrawl: FC<Props> = ({
|
||||
const { t } = useTranslation()
|
||||
const [step, setStep] = useState<Step>(Step.init)
|
||||
const controlFoldOptions = STEP_CONTROL_FOLD_OPTIONS[step]
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const handleSetting = useCallback(() => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('data-source')
|
||||
}, [setSettingsDestination])
|
||||
const checkValid = useCallback(
|
||||
(url: string) => {
|
||||
let errorMsg = ''
|
||||
|
||||
@ -18,15 +18,11 @@ vi.mock('@/context/dataset-detail', () => ({
|
||||
selector({ dataset: { pipeline_id: mockPipelineId } }),
|
||||
}))
|
||||
|
||||
// Mock modal context - context provider requires mocking
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
useModalContextSelector: (selector: (s: Record<string, unknown>) => unknown) =>
|
||||
selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal }),
|
||||
}))
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
// Mock ssePost - API service requires mocking
|
||||
const { mockSsePost } = vi.hoisted(() => ({
|
||||
@ -237,7 +233,7 @@ describe('OnlineDocuments', () => {
|
||||
|
||||
// Reset context values
|
||||
mockPipelineId = 'pipeline-123'
|
||||
mockSetShowAccountSettingModal.mockClear()
|
||||
mockSetSettingsDestination.mockClear()
|
||||
|
||||
// Default mock return values
|
||||
mockUseGetDataSourceAuth.mockReturnValue({
|
||||
@ -612,9 +608,7 @@ describe('OnlineDocuments', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('header-config-btn'))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'data-source',
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
})
|
||||
})
|
||||
|
||||
@ -709,9 +703,7 @@ describe('OnlineDocuments', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('header-config-btn'))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'data-source',
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
})
|
||||
|
||||
it('should handle credential change', () => {
|
||||
|
||||
@ -2,12 +2,15 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-so
|
||||
import type { DataSourceNotionPageMap, DataSourceNotionWorkspace } from '@/models/common'
|
||||
import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useMemo } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import SearchInput from '@/app/components/base/notion-page-selector/search-input'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { DatasourceType } from '@/models/pipeline'
|
||||
@ -35,7 +38,7 @@ const OnlineDocuments = ({
|
||||
}: OnlineDocumentsProps) => {
|
||||
const docLink = useDocLink()
|
||||
const pipelineId = useDatasetDetailContextWithSelector((s) => s.dataset?.pipeline_id)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const { documentsData, searchValue, selectedPagesId, currentCredentialId } =
|
||||
useDataSourceStoreWithSelector(
|
||||
useShallow((state) => ({
|
||||
@ -141,10 +144,8 @@ const OnlineDocuments = ({
|
||||
)
|
||||
|
||||
const handleSetting = useCallback(() => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('data-source')
|
||||
}, [setSettingsDestination])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
|
||||
@ -2,14 +2,13 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-so
|
||||
import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
|
||||
import OnlineDrive from '../index'
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
pipelineId: 'pipeline-123' as string | undefined,
|
||||
docLink: vi.fn((path?: string) => `https://docs.example.com${path || ''}`),
|
||||
openIntegrationsSetting: vi.fn(),
|
||||
setSettingsDestination: vi.fn(),
|
||||
ssePost: vi.fn(),
|
||||
toastError: vi.fn(),
|
||||
useGetDataSourceAuth: vi.fn(),
|
||||
@ -49,9 +48,10 @@ vi.mock('@/context/dataset-detail', () => ({
|
||||
) => selector({ dataset: { pipeline_id: mocks.pipelineId } }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/use-integrations-setting', () => ({
|
||||
useIntegrationsSetting: () => mocks.openIntegrationsSetting,
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mocks.setSettingsDestination] }
|
||||
})
|
||||
|
||||
vi.mock('@/service/base', () => ({
|
||||
ssePost: (...args: unknown[]) => mocks.ssePost(...args),
|
||||
@ -432,9 +432,7 @@ describe('OnlineDrive', () => {
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Configure' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'Change Credential' }))
|
||||
|
||||
expect(mocks.openIntegrationsSetting).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
expect(mocks.setSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
expect(onCredentialChange).toHaveBeenCalledWith('credential-2')
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,10 +3,13 @@ import type { OnlineDriveFile } from '@/models/pipeline'
|
||||
import type { DataSourceNodeCompletedResponse, DataSourceNodeErrorResponse } from '@/types/pipeline'
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { produce } from 'immer'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { DatasourceType, OnlineDriveFileType } from '@/models/pipeline'
|
||||
@ -35,7 +38,7 @@ const OnlineDrive = ({
|
||||
const docLink = useDocLink()
|
||||
const [isInitialMount, setIsInitialMount] = useState(true)
|
||||
const pipelineId = useDatasetDetailContextWithSelector((s) => s.dataset?.pipeline_id)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const {
|
||||
nextPageParameters,
|
||||
breadcrumbs,
|
||||
@ -202,10 +205,8 @@ const OnlineDrive = ({
|
||||
)
|
||||
|
||||
const handleSetting = useCallback(() => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('data-source')
|
||||
}, [setSettingsDestination])
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-y-2">
|
||||
|
||||
@ -2,7 +2,6 @@ import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-so
|
||||
import type { CrawlResultItem } from '@/models/datasets'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { CrawlStep } from '@/models/datasets'
|
||||
import WebsiteCrawl from '../index'
|
||||
|
||||
@ -20,16 +19,11 @@ vi.mock('@/context/dataset-detail', () => ({
|
||||
) => selector({ dataset: { pipeline_id: mockPipelineId } }),
|
||||
}))
|
||||
|
||||
// Mock modal context - context provider requires mocking
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
useModalContextSelector: (
|
||||
selector: (s: { setShowAccountSettingModal: typeof mockSetShowAccountSettingModal }) => unknown,
|
||||
) => selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal }),
|
||||
}))
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
// Mock ssePost - API service requires mocking
|
||||
const { mockSsePost } = vi.hoisted(() => ({
|
||||
@ -257,7 +251,7 @@ describe('WebsiteCrawl', () => {
|
||||
|
||||
// Reset context values
|
||||
mockPipelineId = 'pipeline-123'
|
||||
mockSetShowAccountSettingModal.mockClear()
|
||||
mockSetSettingsDestination.mockClear()
|
||||
|
||||
// Default mock return values
|
||||
mockUseGetDataSourceAuth.mockReturnValue({
|
||||
@ -616,9 +610,7 @@ describe('WebsiteCrawl', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('header-config-btn'))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
})
|
||||
|
||||
it('should have stable handleCredentialChange that resets state', () => {
|
||||
@ -653,9 +645,7 @@ describe('WebsiteCrawl', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('header-config-btn'))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
})
|
||||
|
||||
it('should handle credential change', () => {
|
||||
|
||||
@ -6,12 +6,15 @@ import type {
|
||||
DataSourceNodeErrorResponse,
|
||||
DataSourceNodeProcessingResponse,
|
||||
} from '@/types/pipeline'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useDatasetDetailContextWithSelector } from '@/context/dataset-detail'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { CrawlStep } from '@/models/datasets'
|
||||
@ -52,7 +55,7 @@ const WebsiteCrawl = ({
|
||||
const [crawledNum, setCrawledNum] = useState(0)
|
||||
const [crawlErrorMessage, setCrawlErrorMessage] = useState('')
|
||||
const pipelineId = useDatasetDetailContextWithSelector((s) => s.dataset?.pipeline_id)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const { crawlResult, step, checkedCrawlResult, previewIndex, currentCredentialId } =
|
||||
useDataSourceStoreWithSelector(
|
||||
useShallow((state) => ({
|
||||
@ -158,10 +161,8 @@ const WebsiteCrawl = ({
|
||||
)
|
||||
|
||||
const handleSetting = useCallback(() => {
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
})
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('data-source')
|
||||
}, [setSettingsDestination])
|
||||
|
||||
const handleCredentialChange = useCallback(
|
||||
(credentialId: string) => {
|
||||
|
||||
@ -4,7 +4,7 @@ import DocumentSettings from '../document-settings'
|
||||
|
||||
const mockPush = vi.fn()
|
||||
const mockBack = vi.fn()
|
||||
const mockOpenIntegrationsSetting = vi.fn()
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: mockPush,
|
||||
@ -107,9 +107,10 @@ vi.mock('@/app/components/datasets/create/step-two', () => ({
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/use-integrations-setting', () => ({
|
||||
useIntegrationsSetting: () => mockOpenIntegrationsSetting,
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
describe('DocumentSettings', () => {
|
||||
beforeEach(() => {
|
||||
@ -205,7 +206,7 @@ describe('DocumentSettings', () => {
|
||||
|
||||
fireEvent.click(screen.getByTestId('setting-btn'))
|
||||
|
||||
expect(mockOpenIntegrationsSetting).toHaveBeenCalledWith({ payload: 'provider' })
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -10,16 +10,19 @@ import type {
|
||||
UploadFileIdInfo,
|
||||
WebsiteCrawlInfo,
|
||||
} from '@/models/datasets'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useContext } from 'use-context-selector'
|
||||
import AppUnavailable from '@/app/components/base/app-unavailable'
|
||||
import Loading from '@/app/components/base/loading'
|
||||
import StepTwo from '@/app/components/datasets/create/step-two'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import DatasetDetailContext from '@/context/dataset-detail'
|
||||
import { useRouter } from '@/next/navigation'
|
||||
import {
|
||||
@ -36,12 +39,12 @@ type DocumentSettingsProps = {
|
||||
const DocumentSettings = ({ datasetId, documentId }: DocumentSettingsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const router = useRouter()
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const { indexingTechnique, dataset } = useContext(DatasetDetailContext)
|
||||
const { data: embeddingsDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
|
||||
const handleOpenAccountSetting = useCallback(() => {
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('provider')
|
||||
}, [setSettingsDestination])
|
||||
|
||||
const invalidDocumentList = useInvalidDocumentList(datasetId)
|
||||
const invalidDocumentDetail = useInvalidDocumentDetail()
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect } from 'react'
|
||||
import { EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION } from '@/app/education-apply/constants'
|
||||
import { useSetEducationVerifying } from '@/app/education-apply/storage'
|
||||
import { useSearchParams } from '@/next/navigation'
|
||||
|
||||
export function EducationVerifyActionRecorder() {
|
||||
const searchParams = useSearchParams()
|
||||
const setEducationVerifying = useSetEducationVerifying()
|
||||
|
||||
useEffect(() => {
|
||||
if (searchParams.get('action') === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||
setEducationVerifying('yes')
|
||||
}, [searchParams, setEducationVerifying])
|
||||
|
||||
return null
|
||||
}
|
||||
@ -8,7 +8,6 @@ import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
||||
import { getDocDownloadUrl } from '@/service/common'
|
||||
@ -40,9 +39,14 @@ vi.mock('@/utils/download', () => ({
|
||||
downloadUrl: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
describe('Compliance', () => {
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const toastSuccessSpy = vi.spyOn(toast, 'success').mockReturnValue('toast-success')
|
||||
const toastErrorSpy = vi.spyOn(toast, 'error').mockReturnValue('toast-error')
|
||||
let queryClient: QueryClient
|
||||
@ -66,7 +70,6 @@ describe('Compliance', () => {
|
||||
})
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
} as unknown as ModalContextState)
|
||||
})
|
||||
|
||||
@ -224,9 +227,7 @@ describe('Compliance', () => {
|
||||
fireEvent.click(upgradeBadges[0]!)
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.BILLING,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('billing')
|
||||
})
|
||||
|
||||
// isPending branches: spinner visible, loading button contract, guard blocks second call
|
||||
|
||||
@ -7,7 +7,6 @@ import { fireEvent, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { renderToString } from 'react-dom/server'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import AccountSection from '@/app/components/main-nav/components/account-section'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
@ -82,6 +81,12 @@ vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
}))
|
||||
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
vi.mock('@/service/use-common', async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import('@/service/use-common')>()),
|
||||
useLogout: vi.fn(),
|
||||
@ -176,7 +181,6 @@ const setConsoleState = (value: ConsoleStateFixture) => {
|
||||
describe('AccountDropdown', () => {
|
||||
const mockPush = vi.fn()
|
||||
const mockLogout = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
let deploymentEdition: GetSystemFeaturesResponse['deployment_edition'] = 'COMMUNITY'
|
||||
|
||||
const renderWithRouter = (
|
||||
@ -207,9 +211,7 @@ describe('AccountDropdown', () => {
|
||||
isEducationAccount: false,
|
||||
plan: { type: Plan.sandbox },
|
||||
} as unknown as ProviderContextState)
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
} as unknown as ModalContextState)
|
||||
vi.mocked(useModalContext).mockReturnValue({} as unknown as ModalContextState)
|
||||
vi.mocked(useLogout).mockReturnValue({
|
||||
mutateAsync: mockLogout,
|
||||
} as unknown as ReturnType<typeof useLogout>)
|
||||
@ -288,14 +290,14 @@ describe('AccountDropdown', () => {
|
||||
})
|
||||
|
||||
describe('Settings and Support', () => {
|
||||
it('should trigger setShowAccountSettingModal when settings is clicked', () => {
|
||||
it('should open member settings when settings is clicked', () => {
|
||||
// Act
|
||||
renderWithRouter(<AppSelector />)
|
||||
fireEvent.click(screen.getByRole('button'))
|
||||
fireEvent.click(screen.getByText('common.userProfile.settings'))
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalled()
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('members')
|
||||
})
|
||||
|
||||
it('should open preferences from the account dropdown', () => {
|
||||
@ -305,9 +307,7 @@ describe('AccountDropdown', () => {
|
||||
fireEvent.click(screen.getByText('common.settings.preferences'))
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.PREFERENCES,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('preferences')
|
||||
})
|
||||
|
||||
it('should show Appearance after Preferences in the main nav account dropdown', () => {
|
||||
|
||||
@ -10,10 +10,14 @@ import {
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/tooltip'
|
||||
import { useMutation } from '@tanstack/react-query'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { getDocDownloadUrl } from '@/service/common'
|
||||
@ -95,7 +99,8 @@ type ComplianceDocRowItemProps = {
|
||||
function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProps) {
|
||||
const { t } = useTranslation()
|
||||
const { plan } = useProviderContext()
|
||||
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
|
||||
const { setShowPricingModal } = useModalContext()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const isFreePlan = plan.type === Plan.sandbox
|
||||
|
||||
const { isPending, mutate: downloadCompliance } = useMutation({
|
||||
@ -128,13 +133,13 @@ function ComplianceDocRowItem({ icon, label, docName }: ComplianceDocRowItemProp
|
||||
}
|
||||
|
||||
if (isFreePlan) setShowPricingModal()
|
||||
else setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
||||
else setSettingsDestination('billing')
|
||||
}, [
|
||||
downloadCompliance,
|
||||
isCurrentPlanCanDownload,
|
||||
isFreePlan,
|
||||
isPending,
|
||||
setShowAccountSettingModal,
|
||||
setSettingsDestination,
|
||||
setShowPricingModal,
|
||||
])
|
||||
|
||||
|
||||
@ -11,13 +11,16 @@ import {
|
||||
import { StatusDot } from '@langgenius/dify-ui/status-dot'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import ThemeSwitcher from '@/app/components/base/theme-switcher'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { userProfileAtom } from '@/context/account-state'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { langGeniusVersionInfoAtom } from '@/context/version-state'
|
||||
import { isCurrentWorkspaceOwnerAtom } from '@/context/workspace-state'
|
||||
@ -114,7 +117,7 @@ export function DefaultMenuContent({
|
||||
const langGeniusVersionInfo = useAtomValue(langGeniusVersionInfoAtom)
|
||||
const isCurrentWorkspaceOwner = useAtomValue(isCurrentWorkspaceOwnerAtom)
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -145,7 +148,7 @@ export function DefaultMenuContent({
|
||||
<AccountMenuActionItem
|
||||
iconClassName="i-ri-settings-3-line"
|
||||
label={t(($) => $['userProfile.settings'], { ns: 'common' })}
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })}
|
||||
onClick={() => setSettingsDestination('members')}
|
||||
/>
|
||||
</DropdownMenuGroup>
|
||||
<DropdownMenuSeparator className="my-0! bg-divider-subtle" />
|
||||
|
||||
@ -18,10 +18,13 @@ import {
|
||||
} from '@langgenius/dify-ui/dropdown-menu'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import Link from '@/next/link'
|
||||
@ -107,7 +110,7 @@ export function MainNavMenuContent({ onLogout }: MainNavMenuContentProps) {
|
||||
select: (data) => data.profile,
|
||||
})
|
||||
const { isEducationAccount } = useProviderContext()
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -156,7 +159,7 @@ export function MainNavMenuContent({ onLogout }: MainNavMenuContentProps) {
|
||||
</DropdownMenuLinkItem>
|
||||
<DropdownMenuItem
|
||||
className="mx-0 h-8 gap-1 px-3 py-1"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
|
||||
onClick={() => setSettingsDestination('preferences')}
|
||||
>
|
||||
<MenuItemContent
|
||||
iconClassName="i-ri-equalizer-2-line"
|
||||
|
||||
@ -1,15 +1,26 @@
|
||||
import { isValidSettingsTab } from '../constants'
|
||||
import {
|
||||
isAccountSettingDestination,
|
||||
isIntegrationSettingDestination,
|
||||
settingsQueryParser,
|
||||
} from '../query-params'
|
||||
|
||||
describe('isValidSettingsTab', () => {
|
||||
describe('settingsQueryParser', () => {
|
||||
it.each([
|
||||
['roles-and-permissions', true],
|
||||
['preferences', true],
|
||||
['provider', true],
|
||||
['mcp', true],
|
||||
['agent-strategy', true],
|
||||
['invalid', false],
|
||||
[null, false],
|
||||
])('validates %s', (tab, expected) => {
|
||||
expect(isValidSettingsTab(tab)).toBe(expected)
|
||||
['roles-and-permissions', 'roles-and-permissions'],
|
||||
['preferences', 'preferences'],
|
||||
['provider', 'provider'],
|
||||
['mcp', 'mcp'],
|
||||
['agent-strategy', 'agent-strategy'],
|
||||
['invalid', null],
|
||||
['', null],
|
||||
])('parses %s', (value, expected) => {
|
||||
expect(settingsQueryParser.parse(value)).toBe(expected)
|
||||
})
|
||||
|
||||
it('keeps account and integration destinations in their owning branches', () => {
|
||||
expect(isAccountSettingDestination('members')).toBe(true)
|
||||
expect(isAccountSettingDestination('provider')).toBe(false)
|
||||
expect(isIntegrationSettingDestination('provider')).toBe(true)
|
||||
expect(isIntegrationSettingDestination('members')).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import type { AccountSettingTab } from '../constants'
|
||||
import type { ConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { fireEvent, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useState } from 'react'
|
||||
import { baseProviderContextValue, useProviderContext } from '@/context/provider-context'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
@ -9,7 +8,6 @@ import { renderWithConsoleQuery } from '@/test/console/query-data'
|
||||
import { ACCOUNT_SETTING_TAB } from '../constants'
|
||||
import AccountSetting from '../index'
|
||||
|
||||
const mockResetModelProviderListExpanded = vi.fn()
|
||||
const mockConsoleState = vi.hoisted(() => ({
|
||||
current: null as unknown,
|
||||
}))
|
||||
@ -66,10 +64,6 @@ vi.mock('next-themes', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/atoms', () => ({
|
||||
useResetModelProviderListExpanded: () => mockResetModelProviderListExpanded,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page', () => ({
|
||||
default: ({
|
||||
onSearchTextChange,
|
||||
@ -274,25 +268,6 @@ describe('AccountSetting', () => {
|
||||
expect(screen.getByText('common.settings.preferences'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should keep hidden legacy tab metadata for direct entries', () => {
|
||||
// Act
|
||||
renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.DATA_SOURCE })
|
||||
|
||||
// Assert
|
||||
expect(screen.getByText('common.settings.dataSource'))!.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should normalize legacy language tab entries to preferences', () => {
|
||||
// Act
|
||||
renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.LANGUAGE })
|
||||
|
||||
// Assert
|
||||
const preferencesButton = screen.getByRole('button', { name: 'common.settings.preferences' })
|
||||
expect(preferencesButton.querySelector('.i-ri-equalizer-2-fill')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.account.general')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.account.appearanceLabel')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide sidebar labels on mobile', () => {
|
||||
// Arrange
|
||||
vi.mocked(useBreakpoints).mockReturnValue(MediaType.mobile)
|
||||
@ -691,19 +666,6 @@ describe('AccountSetting', () => {
|
||||
expect(mockOnCancel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should keep provider search controlled at the account setting boundary', async () => {
|
||||
// Arrange
|
||||
const user = userEvent.setup()
|
||||
renderAccountSetting({ initialTab: ACCOUNT_SETTING_TAB.PROVIDER })
|
||||
|
||||
// Act
|
||||
const input = screen.getByRole('searchbox', { name: 'common.operation.search' })
|
||||
await user.type(input, 'test-search')
|
||||
|
||||
// Assert
|
||||
expect(input)!.toHaveValue('test-search')
|
||||
})
|
||||
|
||||
it('should handle scroll event in panel', () => {
|
||||
// Act
|
||||
renderAccountSetting()
|
||||
|
||||
@ -0,0 +1,134 @@
|
||||
import { screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { render } from '@/test/console/render'
|
||||
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||
import { SettingsModal } from '../settings-modal'
|
||||
|
||||
vi.mock('@/app/components/header/account-setting', () => ({
|
||||
default: ({ activeTab, onCancelAction }: { activeTab: string; onCancelAction: () => void }) => (
|
||||
<>
|
||||
<div role="status" aria-label="active account setting tab">
|
||||
{activeTab}
|
||||
</div>
|
||||
<button type="button" onClick={onCancelAction}>
|
||||
cancel account setting
|
||||
</button>
|
||||
</>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/integrations/modal', () => ({
|
||||
default: ({
|
||||
section,
|
||||
onCancel,
|
||||
onSectionChange,
|
||||
}: {
|
||||
section: string
|
||||
onCancel: () => void
|
||||
onSectionChange: (section: 'data-source') => void
|
||||
}) => (
|
||||
<>
|
||||
<div role="status" aria-label="active integration setting section">
|
||||
{section}
|
||||
</div>
|
||||
<button type="button" onClick={() => onSectionChange('data-source')}>
|
||||
switch integration section
|
||||
</button>
|
||||
<button type="button" onClick={onCancel}>
|
||||
cancel integration setting
|
||||
</button>
|
||||
</>
|
||||
),
|
||||
}))
|
||||
|
||||
function PreferencesOpener() {
|
||||
const [settingsDestination, setSettingsDestination] = useQueryState(
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
)
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={settingsDestination === ACCOUNT_SETTING_TAB.PREFERENCES}
|
||||
onClick={() => setSettingsDestination(ACCOUNT_SETTING_TAB.PREFERENCES)}
|
||||
>
|
||||
open preferences
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const renderSettingsModal = (searchParams = '', children?: React.ReactNode) => {
|
||||
const { wrapper, onUrlUpdate } = createNuqsTestWrapper({ searchParams })
|
||||
|
||||
return {
|
||||
...render(
|
||||
<>
|
||||
{children}
|
||||
<SettingsModal />
|
||||
</>,
|
||||
{ wrapper },
|
||||
),
|
||||
onUrlUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
describe('SettingsModal', () => {
|
||||
it('opens account settings with push and closes them with replace', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onUrlUpdate } = renderSettingsModal('', <PreferencesOpener />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'open preferences' }))
|
||||
|
||||
expect(
|
||||
await screen.findByRole('status', { name: 'active account setting tab' }),
|
||||
).toHaveTextContent(ACCOUNT_SETTING_TAB.PREFERENCES)
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('settings')).toBe('preferences')
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].options).toMatchObject({
|
||||
history: 'push',
|
||||
shallow: false,
|
||||
})
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'cancel account setting' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.queryByRole('status', { name: 'active account setting tab' }),
|
||||
).not.toBeInTheDocument()
|
||||
})
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.has('settings')).toBe(false)
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].options).toMatchObject({
|
||||
history: 'replace',
|
||||
shallow: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('renders an integration destination and replaces it when switching sections', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { onUrlUpdate } = renderSettingsModal('?settings=provider')
|
||||
|
||||
expect(
|
||||
await screen.findByRole('status', { name: 'active integration setting section' }),
|
||||
).toHaveTextContent('provider')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'switch integration section' }))
|
||||
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].searchParams.get('settings')).toBe('data-source')
|
||||
expect(onUrlUpdate.mock.calls.at(-1)?.[0].options).toMatchObject({
|
||||
history: 'replace',
|
||||
shallow: true,
|
||||
})
|
||||
})
|
||||
|
||||
it('ignores invalid settings destinations', () => {
|
||||
renderSettingsModal('?settings=unknown')
|
||||
|
||||
expect(screen.queryByRole('status')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -1,3 +1,4 @@
|
||||
import type { SettingsDestination } from '@/app/components/header/account-setting/query-params'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import {
|
||||
@ -5,20 +6,17 @@ import {
|
||||
AUTO_UPDATE_STRATEGY,
|
||||
} from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { ACCOUNT_SETTING_TAB } from '../constants'
|
||||
import UpdateSettingDialogForm from '../update-setting-dialog-form'
|
||||
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContextSelector: (
|
||||
selector: (s: {
|
||||
setShowAccountSettingModal: typeof mockSetShowAccountSettingModal
|
||||
}) => typeof mockSetShowAccountSettingModal,
|
||||
) => {
|
||||
return selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal })
|
||||
},
|
||||
}))
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
let mockSettingsDestination: SettingsDestination | null = null
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return {
|
||||
...actual,
|
||||
useQueryState: () => [mockSettingsDestination, mockSetSettingsDestination],
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const { withSelectorKey, withSelectorKeyProps } = await import('@/test/i18n-mock')
|
||||
@ -64,6 +62,7 @@ vi.mock(
|
||||
describe('UpdateSettingDialogForm', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockSettingsDestination = null
|
||||
})
|
||||
|
||||
it('should open preferences after closing the update setting dialog when timezone link is clicked', () => {
|
||||
@ -96,8 +95,41 @@ describe('UpdateSettingDialogForm', () => {
|
||||
fireEvent.click(screen.getByText('autoUpdate.changeTimezone'))
|
||||
|
||||
expect(onRequestClose).toHaveBeenCalledTimes(1)
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.PREFERENCES,
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('preferences')
|
||||
})
|
||||
|
||||
it('should replace the current destination when timezone link is clicked inside settings', () => {
|
||||
mockSettingsDestination = 'provider'
|
||||
|
||||
render(
|
||||
<UpdateSettingDialogForm
|
||||
autoUpgrade={{
|
||||
strategy_setting: AUTO_UPDATE_STRATEGY.fixOnly,
|
||||
upgrade_time_of_day: 0,
|
||||
upgrade_mode: AUTO_UPDATE_MODE.update_all,
|
||||
exclude_plugins: [],
|
||||
include_plugins: [],
|
||||
}}
|
||||
category={PluginCategoryEnum.tool}
|
||||
plugins={[]}
|
||||
scopeOptions={[{ value: AUTO_UPDATE_MODE.update_all, label: 'All' }]}
|
||||
strategyOptions={[{ value: AUTO_UPDATE_STRATEGY.fixOnly, label: 'Fix only' }]}
|
||||
timezone="UTC"
|
||||
updateTimeValue="00:00"
|
||||
minuteFilter={(minutes) => minutes}
|
||||
onAutoUpgradeChange={vi.fn()}
|
||||
onPluginsChange={vi.fn()}
|
||||
onRequestClose={vi.fn()}
|
||||
onUpdateTimeChange={vi.fn()}
|
||||
renderTimePickerTrigger={() => <button type="button">Pick time</button>}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('autoUpdate.changeTimezone'))
|
||||
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('preferences', {
|
||||
history: 'replace',
|
||||
shallow: true,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,70 +0,0 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { ACCOUNT_SETTING_TAB } from '../constants'
|
||||
import { useIntegrationsSetting } from '../use-integrations-setting'
|
||||
|
||||
const { mockSetShowAccountSettingModal } = vi.hoisted(() => ({
|
||||
mockSetShowAccountSettingModal: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useIntegrationsSetting', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it.each([
|
||||
[ACCOUNT_SETTING_TAB.PROVIDER, 'provider'],
|
||||
[ACCOUNT_SETTING_TAB.DATA_SOURCE, 'data-source'],
|
||||
[ACCOUNT_SETTING_TAB.API_BASED_EXTENSION, 'custom-endpoint'],
|
||||
])('should open integrations settings for migrated tab %s', (tab, section) => {
|
||||
const { result } = renderHook(() => useIntegrationsSetting())
|
||||
|
||||
act(() => {
|
||||
result.current({ payload: tab })
|
||||
})
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: section })
|
||||
})
|
||||
|
||||
it('should open integrations settings from a direct section', () => {
|
||||
const { result } = renderHook(() => useIntegrationsSetting())
|
||||
|
||||
act(() => {
|
||||
result.current({ section: 'mcp' })
|
||||
})
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({ payload: 'mcp' })
|
||||
})
|
||||
|
||||
it('should preserve the agent source for agent-scoped settings', () => {
|
||||
const { result } = renderHook(() => useIntegrationsSetting())
|
||||
|
||||
act(() => {
|
||||
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, source: 'agent' })
|
||||
})
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'provider',
|
||||
source: 'agent',
|
||||
})
|
||||
})
|
||||
|
||||
it('should preserve the cancel callback for migrated integrations settings', () => {
|
||||
const onCancelCallback = vi.fn()
|
||||
const { result } = renderHook(() => useIntegrationsSetting())
|
||||
|
||||
act(() => {
|
||||
result.current({ payload: ACCOUNT_SETTING_TAB.PROVIDER, onCancelCallback })
|
||||
})
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'provider',
|
||||
onCancelCallback,
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,8 +1,5 @@
|
||||
import type { ApiBasedExtensionResponse } from '@dify/contracts/api/console/api-based-extension/types.gen'
|
||||
import type { ModalContextState } from '@/context/modal-context'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { ApiBasedExtensionSelector } from '../selector'
|
||||
|
||||
const { mockApiBasedExtensionsQuery, mockCreateApiBasedExtension, mockUpdateApiBasedExtension } =
|
||||
@ -12,9 +9,11 @@ const { mockApiBasedExtensionsQuery, mockCreateApiBasedExtension, mockUpdateApiB
|
||||
mockUpdateApiBasedExtension: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: vi.fn(),
|
||||
}))
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useDocLink:
|
||||
@ -55,7 +54,6 @@ vi.mock('@langgenius/dify-ui/popover', async () => await import('@/__mocks__/bas
|
||||
|
||||
describe('ApiBasedExtensionSelector', () => {
|
||||
const mockOnChange = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
|
||||
const mockData: ApiBasedExtensionResponse[] = [
|
||||
{ id: '1', name: 'Extension 1', api_endpoint: 'https://api1.test', api_key: 'key1' },
|
||||
@ -64,9 +62,6 @@ describe('ApiBasedExtensionSelector', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
} as unknown as ModalContextState)
|
||||
mockApiBasedExtensionsQuery.mockReturnValue({
|
||||
data: mockData,
|
||||
isPending: false,
|
||||
@ -131,9 +126,7 @@ describe('ApiBasedExtensionSelector', () => {
|
||||
fireEvent.click(manageButton)
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('custom-endpoint')
|
||||
})
|
||||
|
||||
it('should open add modal when clicking add button and close it after save', async () => {
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@langgenius/dify-ui/popover'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { ApiBasedExtensionModal } from './modal'
|
||||
|
||||
@ -16,7 +19,7 @@ export function ApiBasedExtensionSelector({ value, onChange }: ApiBasedExtension
|
||||
const { t } = useTranslation()
|
||||
const [open, setOpen] = useState(false)
|
||||
const [addModalOpen, setAddModalOpen] = useState(false)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const { data: apiBasedExtensions = [] } = useQuery(
|
||||
consoleQuery.apiBasedExtension.get.queryOptions(),
|
||||
)
|
||||
@ -84,9 +87,7 @@ export function ApiBasedExtensionSelector({ value, onChange }: ApiBasedExtension
|
||||
className="flex cursor-pointer items-center border-none bg-transparent p-0 text-xs text-text-accent"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION,
|
||||
})
|
||||
setSettingsDestination('custom-endpoint')
|
||||
}}
|
||||
>
|
||||
{t(($) => $['apiBasedExtension.selector.manage'], { ns: 'common' })}
|
||||
|
||||
@ -1,69 +1,21 @@
|
||||
import type { IntegrationSection } from '@/app/components/integrations/routes'
|
||||
import { INTEGRATION_SECTION_VALUES } from '@/app/components/integrations/routes'
|
||||
|
||||
export const ACCOUNT_SETTING_MODAL_ACTION = 'showSettings'
|
||||
|
||||
export const ACCOUNT_SETTING_TAB = {
|
||||
PROVIDER: 'provider',
|
||||
MEMBERS: 'members',
|
||||
ROLES_AND_PERMISSIONS: 'roles-and-permissions',
|
||||
PERMISSION_SET: 'permission-set',
|
||||
BILLING: 'billing',
|
||||
WORKFLOW_LOG_ARCHIVES: 'workflow-log-archives',
|
||||
DATA_SOURCE: 'data-source',
|
||||
API_BASED_EXTENSION: 'custom-endpoint',
|
||||
CUSTOM: 'custom',
|
||||
PREFERENCES: 'preferences',
|
||||
LANGUAGE: 'language',
|
||||
} as const
|
||||
|
||||
export type AccountSettingTab = (typeof ACCOUNT_SETTING_TAB)[keyof typeof ACCOUNT_SETTING_TAB]
|
||||
|
||||
export const DEFAULT_ACCOUNT_SETTING_TAB = ACCOUNT_SETTING_TAB.MEMBERS
|
||||
|
||||
const WORKSPACE_SETTING_TAB_VALUES = [
|
||||
export const ACCOUNT_SETTING_TAB_VALUES = [
|
||||
ACCOUNT_SETTING_TAB.MEMBERS,
|
||||
ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS,
|
||||
ACCOUNT_SETTING_TAB.PERMISSION_SET,
|
||||
ACCOUNT_SETTING_TAB.BILLING,
|
||||
ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES,
|
||||
ACCOUNT_SETTING_TAB.CUSTOM,
|
||||
] as const
|
||||
|
||||
export type WorkspaceSettingTab = (typeof WORKSPACE_SETTING_TAB_VALUES)[number]
|
||||
|
||||
const USER_SETTING_TAB_VALUES = [
|
||||
ACCOUNT_SETTING_TAB.PREFERENCES,
|
||||
ACCOUNT_SETTING_TAB.LANGUAGE,
|
||||
] as const
|
||||
|
||||
export type UserSettingTab = (typeof USER_SETTING_TAB_VALUES)[number]
|
||||
|
||||
export type IntegrationSettingTab = IntegrationSection
|
||||
|
||||
export const SETTINGS_TAB_VALUES = [
|
||||
...WORKSPACE_SETTING_TAB_VALUES,
|
||||
...USER_SETTING_TAB_VALUES,
|
||||
...INTEGRATION_SECTION_VALUES,
|
||||
] as const
|
||||
|
||||
export type SettingsTab = (typeof SETTINGS_TAB_VALUES)[number]
|
||||
export const isValidSettingsTab = (tab: string | null): tab is SettingsTab => {
|
||||
if (!tab) return false
|
||||
return SETTINGS_TAB_VALUES.includes(tab as SettingsTab)
|
||||
}
|
||||
|
||||
export const isWorkspaceSettingTab = (tab: SettingsTab | null): tab is WorkspaceSettingTab => {
|
||||
if (!tab) return false
|
||||
return WORKSPACE_SETTING_TAB_VALUES.includes(tab as WorkspaceSettingTab)
|
||||
}
|
||||
|
||||
export const isUserSettingTab = (tab: SettingsTab | null): tab is UserSettingTab => {
|
||||
if (!tab) return false
|
||||
return USER_SETTING_TAB_VALUES.includes(tab as UserSettingTab)
|
||||
}
|
||||
|
||||
export const isIntegrationSettingTab = (tab: SettingsTab | null): tab is IntegrationSettingTab => {
|
||||
if (!tab) return false
|
||||
return INTEGRATION_SECTION_VALUES.includes(tab as IntegrationSection)
|
||||
}
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
import type { AccountSettingTab } from './constants'
|
||||
import type { IntegrationSection } from '@/app/components/integrations/routes'
|
||||
import { ACCOUNT_SETTING_TAB } from './constants'
|
||||
|
||||
export const integrationSectionByMovedAccountSettingTab = {
|
||||
[ACCOUNT_SETTING_TAB.PROVIDER]: 'provider',
|
||||
[ACCOUNT_SETTING_TAB.DATA_SOURCE]: 'data-source',
|
||||
[ACCOUNT_SETTING_TAB.API_BASED_EXTENSION]: 'custom-endpoint',
|
||||
} as const satisfies Partial<Record<AccountSettingTab, IntegrationSection>>
|
||||
|
||||
export type MovedAccountSettingTab = keyof typeof integrationSectionByMovedAccountSettingTab
|
||||
@ -5,7 +5,7 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { ScrollArea } from '@langgenius/dify-ui/scroll-area'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useRef, useState } from 'react'
|
||||
import { useRef } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import BillingPage from '@/app/components/billing/billing-page'
|
||||
import CustomPage from '@/app/components/custom/custom-page'
|
||||
@ -21,11 +21,7 @@ import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import AccessRulesPage from './access-rules-page'
|
||||
import { ApiBasedExtensionPage } from './api-based-extension-page'
|
||||
import DataSourcePage from './data-source-page-new'
|
||||
import MembersPage from './members-page'
|
||||
import ModelProviderPage from './model-provider-page'
|
||||
import { useResetModelProviderListExpanded } from './model-provider-page/atoms'
|
||||
import PermissionsPage from './permissions-page'
|
||||
import PreferencePage from './preference-page'
|
||||
import WorkflowLogArchivesPage from './workflow-log-archives-page'
|
||||
@ -54,7 +50,6 @@ export default function AccountSetting({
|
||||
activeTab,
|
||||
onTabChangeAction,
|
||||
}: IAccountSettingProps) {
|
||||
const resetModelProviderListExpanded = useResetModelProviderListExpanded()
|
||||
const { t } = useTranslation()
|
||||
const { enableBilling, enableReplaceWebAppLogo } = useProviderContext()
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
@ -67,34 +62,22 @@ export default function AccountSetting({
|
||||
const canViewBilling = enableBilling && !isCurrentWorkspaceDatasetOperator
|
||||
const canViewWorkflowLogArchives =
|
||||
systemFeatures.deployment_edition === 'CLOUD' && isCurrentWorkspaceManager
|
||||
// Keep legacy `language` deep links opening Preferences during the tab rename migration.
|
||||
const normalizedActiveTab =
|
||||
activeTab === ACCOUNT_SETTING_TAB.LANGUAGE ? ACCOUNT_SETTING_TAB.PREFERENCES : activeTab
|
||||
const activeMenu = (() => {
|
||||
if (normalizedActiveTab === ACCOUNT_SETTING_TAB.BILLING && !canViewBilling)
|
||||
if (activeTab === ACCOUNT_SETTING_TAB.BILLING && !canViewBilling)
|
||||
return ACCOUNT_SETTING_TAB.PREFERENCES
|
||||
if (
|
||||
normalizedActiveTab === ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES &&
|
||||
!canViewWorkflowLogArchives
|
||||
)
|
||||
if (activeTab === ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES && !canViewWorkflowLogArchives)
|
||||
return ACCOUNT_SETTING_TAB.MEMBERS
|
||||
if (
|
||||
(normalizedActiveTab === ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS ||
|
||||
normalizedActiveTab === ACCOUNT_SETTING_TAB.PERMISSION_SET) &&
|
||||
(activeTab === ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS ||
|
||||
activeTab === ACCOUNT_SETTING_TAB.PERMISSION_SET) &&
|
||||
!canManageWorkspaceRoles
|
||||
)
|
||||
return ACCOUNT_SETTING_TAB.MEMBERS
|
||||
return normalizedActiveTab
|
||||
return activeTab
|
||||
})()
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const settingItems: GroupItem[] = [
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
name: t(($) => $['settings.provider'], { ns: 'common' }),
|
||||
icon: <span className={cn('i-ri-brain-2-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-brain-2-fill', iconClassName)} />,
|
||||
},
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.MEMBERS,
|
||||
name: t(($) => $['settings.members'], { ns: 'common' }),
|
||||
@ -128,18 +111,6 @@ export default function AccountSetting({
|
||||
icon: <span className={cn('i-ri-archive-drawer-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-archive-drawer-fill', iconClassName)} />,
|
||||
},
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.DATA_SOURCE,
|
||||
name: t(($) => $['settings.dataSource'], { ns: 'common' }),
|
||||
icon: <span className={cn('i-ri-database-2-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-database-2-fill', iconClassName)} />,
|
||||
},
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.API_BASED_EXTENSION,
|
||||
name: t(($) => $['settings.customEndpoint'], { ns: 'common' }),
|
||||
icon: <span className={cn('i-ri-puzzle-2-line', iconClassName)} />,
|
||||
activeIcon: <span className={cn('i-ri-puzzle-2-fill', iconClassName)} />,
|
||||
},
|
||||
{
|
||||
key: ACCOUNT_SETTING_TAB.CUSTOM,
|
||||
name: t(($) => $.custom, { ns: 'custom' }),
|
||||
@ -193,31 +164,15 @@ export default function AccountSetting({
|
||||
},
|
||||
]
|
||||
|
||||
const [searchValue, setSearchValue] = useState<string>('')
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(tab: AccountSettingTab) => {
|
||||
if (tab === ACCOUNT_SETTING_TAB.PROVIDER) resetModelProviderListExpanded()
|
||||
|
||||
onTabChangeAction(tab)
|
||||
},
|
||||
[onTabChangeAction, resetModelProviderListExpanded],
|
||||
)
|
||||
|
||||
const handleClose = useCallback(() => {
|
||||
resetModelProviderListExpanded()
|
||||
onCancelAction()
|
||||
}, [onCancelAction, resetModelProviderListExpanded])
|
||||
|
||||
return (
|
||||
<MenuDialog show onClose={handleClose}>
|
||||
<MenuDialog show onClose={onCancelAction}>
|
||||
<div className="fixed top-6 right-6 z-20 flex shrink-0 flex-col items-center">
|
||||
<Button
|
||||
variant="tertiary"
|
||||
size="large"
|
||||
className="px-2"
|
||||
aria-label={t(($) => $['operation.close'], { ns: 'common' })}
|
||||
onClick={handleClose}
|
||||
onClick={onCancelAction}
|
||||
>
|
||||
<span className="i-ri-close-line size-5" />
|
||||
</Button>
|
||||
@ -257,7 +212,7 @@ export default function AccountSetting({
|
||||
aria-label={item.name}
|
||||
title={item.name}
|
||||
onClick={() => {
|
||||
handleTabChange(item.key)
|
||||
onTabChangeAction(item.key)
|
||||
}}
|
||||
>
|
||||
{activeMenu === item.key ? item.activeIcon : item.icon}
|
||||
@ -289,9 +244,6 @@ export default function AccountSetting({
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-full min-w-0 px-4 pt-6 sm:px-8">
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.PROVIDER && (
|
||||
<ModelProviderPage searchText={searchValue} onSearchTextChange={setSearchValue} />
|
||||
)}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.MEMBERS && <MembersPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.ROLES_AND_PERMISSIONS && (
|
||||
<PermissionsPage containerRef={scrollContainerRef} />
|
||||
@ -301,8 +253,6 @@ export default function AccountSetting({
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.WORKFLOW_LOG_ARCHIVES && (
|
||||
<WorkflowLogArchivesPage />
|
||||
)}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.DATA_SOURCE && <DataSourcePage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.API_BASED_EXTENSION && <ApiBasedExtensionPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.CUSTOM && <CustomPage />}
|
||||
{activeMenu === ACCOUNT_SETTING_TAB.PREFERENCES && <PreferencePage />}
|
||||
</div>
|
||||
|
||||
@ -5,7 +5,6 @@ import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
useExpandModelProviderList,
|
||||
useModelProviderListExpanded,
|
||||
useResetModelProviderListExpanded,
|
||||
useSetModelProviderListExpanded,
|
||||
} from '../atoms'
|
||||
|
||||
@ -169,73 +168,6 @@ describe('atoms', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Reset hook: clears all expanded state back to empty
|
||||
describe('useResetModelProviderListExpanded', () => {
|
||||
it('should reset all expanded providers to false', () => {
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
openaiExpanded: useModelProviderListExpanded('openai'),
|
||||
anthropicExpanded: useModelProviderListExpanded('anthropic'),
|
||||
expand: useExpandModelProviderList(),
|
||||
reset: useResetModelProviderListExpanded(),
|
||||
}),
|
||||
{ wrapper },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.expand('openai')
|
||||
})
|
||||
act(() => {
|
||||
result.current.expand('anthropic')
|
||||
})
|
||||
act(() => {
|
||||
result.current.reset()
|
||||
})
|
||||
|
||||
expect(result.current.openaiExpanded).toBe(false)
|
||||
expect(result.current.anthropicExpanded).toBe(false)
|
||||
})
|
||||
|
||||
it('should be safe to call when no providers are expanded', () => {
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
expanded: useModelProviderListExpanded('openai'),
|
||||
reset: useResetModelProviderListExpanded(),
|
||||
}),
|
||||
{ wrapper },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.reset()
|
||||
})
|
||||
|
||||
expect(result.current.expanded).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow re-expanding providers after reset', () => {
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
expanded: useModelProviderListExpanded('openai'),
|
||||
expand: useExpandModelProviderList(),
|
||||
reset: useResetModelProviderListExpanded(),
|
||||
}),
|
||||
{ wrapper },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.expand('openai')
|
||||
})
|
||||
act(() => {
|
||||
result.current.reset()
|
||||
})
|
||||
act(() => {
|
||||
result.current.expand('openai')
|
||||
})
|
||||
|
||||
expect(result.current.expanded).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
// Cross-hook interaction: verify hooks cooperate through the shared atom
|
||||
describe('Cross-hook interaction', () => {
|
||||
it('should reflect state set by useSetModelProviderListExpanded in useModelProviderListExpanded', () => {
|
||||
@ -290,26 +222,6 @@ describe('atoms', () => {
|
||||
})
|
||||
expect(result.current.expanded).toBe(false)
|
||||
})
|
||||
|
||||
it('should reset state set by useSetModelProviderListExpanded via useResetModelProviderListExpanded', () => {
|
||||
const { result } = renderHook(
|
||||
() => ({
|
||||
expanded: useModelProviderListExpanded('openai'),
|
||||
setExpanded: useSetModelProviderListExpanded('openai'),
|
||||
reset: useResetModelProviderListExpanded(),
|
||||
}),
|
||||
{ wrapper },
|
||||
)
|
||||
|
||||
act(() => {
|
||||
result.current.setExpanded(true)
|
||||
})
|
||||
act(() => {
|
||||
result.current.reset()
|
||||
})
|
||||
|
||||
expect(result.current.expanded).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// selectAtom granularity: changing one provider should not affect unrelated reads
|
||||
|
||||
@ -26,8 +26,8 @@ type MockReferenceSetting = {
|
||||
}
|
||||
}
|
||||
|
||||
const { mockSetAccountSettingModal, mockSaveAutoUpgrade } = vi.hoisted(() => ({
|
||||
mockSetAccountSettingModal: vi.fn(),
|
||||
const { mockSetSettingsDestination, mockSaveAutoUpgrade } = vi.hoisted(() => ({
|
||||
mockSetSettingsDestination: vi.fn(),
|
||||
mockSaveAutoUpgrade: vi.fn(),
|
||||
}))
|
||||
|
||||
@ -289,11 +289,10 @@ vi.mock('@langgenius/dify-ui/dialog', () => ({
|
||||
DialogCloseButton: () => <button type="button" aria-label="close" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContextSelector: (
|
||||
selector: (state: { setShowAccountSettingModal: typeof mockSetAccountSettingModal }) => unknown,
|
||||
) => selector({ setShowAccountSettingModal: mockSetAccountSettingModal }),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/base/date-and-time-picker/time-picker', () => ({
|
||||
default: ({
|
||||
|
||||
@ -25,8 +25,3 @@ export function useExpandModelProviderList() {
|
||||
[set],
|
||||
)
|
||||
}
|
||||
|
||||
export function useResetModelProviderListExpanded() {
|
||||
const set = useSetAtom(expandedAtom)
|
||||
return useCallback(() => set({}), [set])
|
||||
}
|
||||
|
||||
@ -16,16 +16,17 @@ import Popup from '../popup'
|
||||
|
||||
let mockLanguage = 'en_US'
|
||||
|
||||
const mockSetShowAccountSettingModal = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockSearchParams = vi.hoisted(() => ({
|
||||
current: new URLSearchParams(),
|
||||
}))
|
||||
const mockSetSettingsDestination = vi.hoisted(() => vi.fn())
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return {
|
||||
...actual,
|
||||
useQueryState: () => [mockSearchParams.current.get('settings'), mockSetSettingsDestination],
|
||||
}
|
||||
})
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useSearchParams: () => mockSearchParams.current,
|
||||
}))
|
||||
@ -1052,13 +1053,11 @@ describe('Popup', () => {
|
||||
fireEvent.click(screen.getByText('common.modelProvider.selector.modelProviderSettings'))
|
||||
|
||||
expect(onHide).toHaveBeenCalled()
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'provider',
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
})
|
||||
|
||||
it('should hide provider settings footer when current account settings tab is provider', () => {
|
||||
mockSearchParams.current = new URLSearchParams('action=showSettings&tab=provider')
|
||||
it('should hide provider settings footer when provider settings are already open', () => {
|
||||
mockSearchParams.current = new URLSearchParams('settings=provider')
|
||||
|
||||
renderPopup(<PopupHarness modelList={[makeModel()]} onHide={vi.fn()} />)
|
||||
|
||||
@ -1090,20 +1089,18 @@ describe('Popup', () => {
|
||||
|
||||
fireEvent.click(screen.getByText(/modelProvider\.selector\.configure/))
|
||||
expect(onHide).toHaveBeenCalled()
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'provider',
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('provider')
|
||||
})
|
||||
|
||||
it('should only close the empty state selector when current account settings tab is provider', () => {
|
||||
mockSearchParams.current = new URLSearchParams('action=showSettings&tab=provider')
|
||||
it('should only close the empty state selector when provider settings are already open', () => {
|
||||
mockSearchParams.current = new URLSearchParams('settings=provider')
|
||||
const onHide = vi.fn()
|
||||
renderPopup(<PopupHarness modelList={[]} onHide={onHide} />)
|
||||
|
||||
fireEvent.click(screen.getByText(/modelProvider\.selector\.configure/))
|
||||
|
||||
expect(onHide).toHaveBeenCalled()
|
||||
expect(mockSetShowAccountSettingModal).not.toHaveBeenCalled()
|
||||
expect(mockSetSettingsDestination).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should render marketplace providers that are not installed', () => {
|
||||
|
||||
@ -33,7 +33,6 @@ type ModelSelectorProps = {
|
||||
hideProviderSettingsFooter?: boolean
|
||||
onConfigureEmptyState?: () => void
|
||||
onOpenMarketplace?: () => void
|
||||
providerSettingsSource?: 'agent'
|
||||
showModelMeta?: boolean
|
||||
modelPredicate?: ModelSelectorModelPredicate
|
||||
modelSuggestionPredicate?: ModelSelectorModelPredicate
|
||||
@ -52,7 +51,6 @@ function ModelSelector({
|
||||
hideProviderSettingsFooter,
|
||||
onConfigureEmptyState,
|
||||
onOpenMarketplace,
|
||||
providerSettingsSource,
|
||||
showModelMeta,
|
||||
modelPredicate,
|
||||
modelSuggestionPredicate,
|
||||
@ -175,7 +173,6 @@ function ModelSelector({
|
||||
modelList={modelList}
|
||||
scopeFeatures={scopeFeatures}
|
||||
hideProviderSettingsFooter={hideProviderSettingsFooter}
|
||||
providerSettingsSource={providerSettingsSource}
|
||||
modelPredicate={modelPredicate}
|
||||
modelSuggestionPredicate={modelSuggestionPredicate}
|
||||
onConfigureEmptyState={onConfigureEmptyState ? handleConfigureEmptyState : undefined}
|
||||
|
||||
@ -10,19 +10,18 @@ import {
|
||||
} from '@langgenius/dify-ui/preview-card'
|
||||
import { useQuery, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
ACCOUNT_SETTING_MODAL_ACTION,
|
||||
ACCOUNT_SETTING_TAB,
|
||||
} from '@/app/components/header/account-setting/constants'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import checkTaskStatus from '@/app/components/plugins/install-plugin/base/check-task-status'
|
||||
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
|
||||
import useWorkspacePluginInstallPermission from '@/app/components/plugins/install-plugin/hooks/use-workspace-plugin-install-permission'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { useSearchParams } from '@/next/navigation'
|
||||
import { consoleQuery } from '@/service/client'
|
||||
import { useInstallPackageFromMarketPlace } from '@/service/use-plugins'
|
||||
import {
|
||||
@ -63,7 +62,6 @@ export type PopupProps = {
|
||||
modelList: Model[]
|
||||
scopeFeatures?: ModelFeatureEnum[]
|
||||
hideProviderSettingsFooter?: boolean
|
||||
providerSettingsSource?: 'agent'
|
||||
modelPredicate?: ModelSelectorModelPredicate
|
||||
modelSuggestionPredicate?: ModelSelectorModelPredicate
|
||||
onConfigureEmptyState?: () => void
|
||||
@ -77,7 +75,6 @@ function Popup({
|
||||
modelList,
|
||||
scopeFeatures = [],
|
||||
hideProviderSettingsFooter,
|
||||
providerSettingsSource,
|
||||
modelPredicate,
|
||||
modelSuggestionPredicate,
|
||||
onConfigureEmptyState,
|
||||
@ -86,7 +83,10 @@ function Popup({
|
||||
onHide,
|
||||
}: PopupProps) {
|
||||
const { t } = useTranslation()
|
||||
const searchParams = useSearchParams()
|
||||
const [settingsDestination, setSettingsDestination] = useQueryState(
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
)
|
||||
const { theme } = useTheme()
|
||||
const language = useLanguage()
|
||||
const previewCardHandle = useMemo(
|
||||
@ -95,7 +95,6 @@ function Popup({
|
||||
)
|
||||
const [marketplaceCollapsed, setMarketplaceCollapsed] = useState(false)
|
||||
const [showIncompatibleModels, setShowIncompatibleModels] = useState(false)
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const { modelProviders } = useProviderContext()
|
||||
const { data: enableMarketplace } = useSuspenseQuery({
|
||||
...systemFeaturesQueryOptions(),
|
||||
@ -255,17 +254,12 @@ function Popup({
|
||||
|
||||
const handleOpenSettings = useCallback(() => {
|
||||
onHide()
|
||||
openIntegrationsSetting({
|
||||
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
source: providerSettingsSource,
|
||||
})
|
||||
}, [onHide, openIntegrationsSetting, providerSettingsSource])
|
||||
setSettingsDestination('provider')
|
||||
}, [onHide, setSettingsDestination])
|
||||
const handleClosePreviewCard = useCallback(() => {
|
||||
previewCardHandle.close()
|
||||
}, [previewCardHandle])
|
||||
const isProviderSettingsCurrentPage =
|
||||
searchParams?.get('action') === ACCOUNT_SETTING_MODAL_ACTION &&
|
||||
searchParams?.get('tab') === ACCOUNT_SETTING_TAB.PROVIDER
|
||||
const isProviderSettingsCurrentPage = settingsDestination === 'provider'
|
||||
const handleConfigureEmptyState =
|
||||
onConfigureEmptyState ?? (isProviderSettingsCurrentPage ? onHide : handleOpenSettings)
|
||||
|
||||
|
||||
28
web/app/components/header/account-setting/query-params.ts
Normal file
28
web/app/components/header/account-setting/query-params.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import type { inferParserType } from 'nuqs'
|
||||
import { parseAsStringLiteral } from 'nuqs'
|
||||
import { INTEGRATION_SECTION_VALUES } from '@/app/components/integrations/routes'
|
||||
import { ACCOUNT_SETTING_TAB_VALUES } from './constants'
|
||||
|
||||
export const settingsQueryParamName = 'settings'
|
||||
|
||||
// Opening the full-screen settings surface is the common write. It creates a history entry and
|
||||
// opts into a Next.js navigation so browser Back updates both the URL and the nuqs snapshot.
|
||||
// Closing and switching destinations stay shallow and replace history at the modal owner.
|
||||
export const settingsQueryParser = parseAsStringLiteral([
|
||||
...ACCOUNT_SETTING_TAB_VALUES,
|
||||
...INTEGRATION_SECTION_VALUES,
|
||||
] as const).withOptions({ history: 'push', shallow: false })
|
||||
|
||||
export type SettingsDestination = inferParserType<typeof settingsQueryParser>
|
||||
|
||||
export const isAccountSettingDestination = (
|
||||
destination: SettingsDestination | null,
|
||||
): destination is (typeof ACCOUNT_SETTING_TAB_VALUES)[number] => {
|
||||
return ACCOUNT_SETTING_TAB_VALUES.some((value) => value === destination)
|
||||
}
|
||||
|
||||
export const isIntegrationSettingDestination = (
|
||||
destination: SettingsDestination | null,
|
||||
): destination is (typeof INTEGRATION_SECTION_VALUES)[number] => {
|
||||
return INTEGRATION_SECTION_VALUES.some((value) => value === destination)
|
||||
}
|
||||
54
web/app/components/header/account-setting/settings-modal.tsx
Normal file
54
web/app/components/header/account-setting/settings-modal.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
'use client'
|
||||
|
||||
import type { SettingsDestination } from './query-params'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import {
|
||||
isAccountSettingDestination,
|
||||
isIntegrationSettingDestination,
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from './query-params'
|
||||
|
||||
// This controller is mounted globally, so concrete settings surfaces must stay lazy and only
|
||||
// load after the URL selects their destination.
|
||||
const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
|
||||
ssr: false,
|
||||
})
|
||||
const IntegrationsSettingModal = dynamic(() => import('@/app/components/integrations/modal'), {
|
||||
ssr: false,
|
||||
})
|
||||
|
||||
export function SettingsModal() {
|
||||
const [destination, setDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
|
||||
const handleClose = () => {
|
||||
setDestination(null, { history: 'replace', shallow: true })
|
||||
}
|
||||
|
||||
const handleDestinationChange = (nextDestination: SettingsDestination) => {
|
||||
setDestination(nextDestination, { history: 'replace', shallow: true })
|
||||
}
|
||||
|
||||
if (isAccountSettingDestination(destination)) {
|
||||
return (
|
||||
<AccountSetting
|
||||
activeTab={destination}
|
||||
onCancelAction={handleClose}
|
||||
onTabChangeAction={handleDestinationChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isIntegrationSettingDestination(destination)) {
|
||||
return (
|
||||
<IntegrationsSettingModal
|
||||
section={destination}
|
||||
onCancel={handleClose}
|
||||
onSectionChange={handleDestinationChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
@ -5,17 +5,20 @@ import type { dayjsToTimeOfDay } from '@/app/components/plugins/reference-settin
|
||||
import type { PluginCategoryEnum } from '@/app/components/plugins/types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { RadioGroup } from '@langgenius/dify-ui/radio'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import TimePicker from '@/app/components/base/date-and-time-picker/time-picker'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import PluginsPicker from '@/app/components/plugins/reference-setting-modal/auto-update-setting/plugins-picker'
|
||||
import {
|
||||
AUTO_UPDATE_MODE,
|
||||
AUTO_UPDATE_STRATEGY,
|
||||
} from '@/app/components/plugins/reference-setting-modal/auto-update-setting/types'
|
||||
import { convertLocalSecondsToUTCDaySeconds } from '@/app/components/plugins/reference-setting-modal/auto-update-setting/utils'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import UpdateSettingOptionCard from './update-setting-option-card'
|
||||
|
||||
type Option<Value extends string> = {
|
||||
@ -49,7 +52,10 @@ function SettingTimeZone({
|
||||
children?: ReactNode
|
||||
onRequestClose: () => void
|
||||
}) {
|
||||
const setShowAccountSettingModal = useModalContextSelector((s) => s.setShowAccountSettingModal)
|
||||
const [settingsDestination, setSettingsDestination] = useQueryState(
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
)
|
||||
|
||||
return (
|
||||
<button
|
||||
@ -57,7 +63,9 @@ function SettingTimeZone({
|
||||
className="cursor-pointer border-none bg-transparent p-0 text-left body-xs-regular text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => {
|
||||
onRequestClose()
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })
|
||||
if (settingsDestination)
|
||||
setSettingsDestination('preferences', { history: 'replace', shallow: true })
|
||||
else setSettingsDestination('preferences')
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import type { MovedAccountSettingTab } from './destinations'
|
||||
import type { IntegrationSection } from '@/app/components/integrations/routes'
|
||||
import { useCallback } from 'react'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import { integrationSectionByMovedAccountSettingTab } from './destinations'
|
||||
|
||||
type IntegrationsSettingState =
|
||||
| { payload: MovedAccountSettingTab; source?: 'agent'; onCancelCallback?: () => void }
|
||||
| { section: IntegrationSection; source?: 'agent'; onCancelCallback?: () => void }
|
||||
|
||||
export const useIntegrationsSetting = () => {
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
|
||||
return useCallback(
|
||||
(state: IntegrationsSettingState) => {
|
||||
const section =
|
||||
'section' in state
|
||||
? state.section
|
||||
: integrationSectionByMovedAccountSettingTab[state.payload]
|
||||
|
||||
if (section) {
|
||||
setShowAccountSettingModal({
|
||||
payload: section,
|
||||
...(state.source ? { source: state.source } : {}),
|
||||
...(state.onCancelCallback ? { onCancelCallback: state.onCancelCallback } : {}),
|
||||
})
|
||||
}
|
||||
},
|
||||
[setShowAccountSettingModal],
|
||||
)
|
||||
}
|
||||
@ -1,29 +1,25 @@
|
||||
'use client'
|
||||
|
||||
import type { IntegrationSection } from '@/app/components/integrations/routes'
|
||||
import type { IntegrationSection } from './routes'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useCallback } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import MenuDialog from '@/app/components/header/account-setting/menu-dialog'
|
||||
import IntegrationsPage from '@/app/components/integrations/page'
|
||||
import { getMarketplaceUrl } from '@/utils/var'
|
||||
import IntegrationsPage from './page'
|
||||
|
||||
type IntegrationsSettingModalProps = {
|
||||
section: IntegrationSection
|
||||
source?: 'agent'
|
||||
onCancel: () => void
|
||||
onSectionChange: (section: IntegrationSection) => void
|
||||
}
|
||||
|
||||
export default function IntegrationsSettingModal({
|
||||
section,
|
||||
source,
|
||||
onCancel,
|
||||
onSectionChange,
|
||||
}: IntegrationsSettingModalProps) {
|
||||
const { t } = useTranslation()
|
||||
const isAgentSource = source === 'agent'
|
||||
const handleSwitchToMarketplace = useCallback((path: string) => {
|
||||
window.open(
|
||||
getMarketplaceUrl(path, undefined, { source: window.location.origin }),
|
||||
@ -33,18 +29,8 @@ export default function IntegrationsSettingModal({
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<MenuDialog
|
||||
show
|
||||
backdropClassName={isAgentSource ? 'bg-background-overlay' : undefined}
|
||||
className={isAgentSource ? 'bg-transparent backdrop-blur-none' : undefined}
|
||||
onClose={onCancel}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6',
|
||||
isAgentSource && 'w-full p-6',
|
||||
)}
|
||||
>
|
||||
<MenuDialog show onClose={onCancel}>
|
||||
<div className="mx-auto flex h-dvh w-[min(1440px,calc(100vw-48px))] shrink-0 py-6">
|
||||
<div className="relative flex min-h-0 w-full shrink-0 overflow-hidden rounded-2xl border border-divider-subtle bg-components-panel-bg shadow-2xl">
|
||||
<IntegrationsPage
|
||||
section={section}
|
||||
@ -343,7 +343,11 @@ vi.mock('@/config', async (importOriginal) => {
|
||||
|
||||
const mockPush = vi.fn()
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
const mockUninstall = vi.fn()
|
||||
const mockUpdatePinStatus = vi.fn()
|
||||
let mockPathname = '/apps'
|
||||
@ -537,7 +541,6 @@ describe('MainNav', () => {
|
||||
} as ProviderContextState)
|
||||
;(useModalContext as Mock).mockReturnValue({
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
} as unknown as ModalContextState)
|
||||
;(useGetInstalledApps as Mock).mockImplementation(() => ({
|
||||
isPending: mockInstalledAppsPending,
|
||||
@ -1113,24 +1116,18 @@ describe('MainNav', () => {
|
||||
expect(
|
||||
screen.getByRole('link', { name: /common\.mainNav\.workspace\.credits|7,500 credits/ }),
|
||||
).toHaveAttribute('href', '/integrations/model-provider')
|
||||
expect(mockSetShowAccountSettingModal).not.toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.PROVIDER,
|
||||
})
|
||||
expect(mockSetSettingsDestination).not.toHaveBeenCalledWith('provider')
|
||||
|
||||
fireEvent.click(screen.getByText('billing.upgradeBtn.plain'))
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' }))
|
||||
fireEvent.click(await screen.findByText('common.mainNav.workspace.settings'))
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.BILLING,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.BILLING)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' }))
|
||||
fireEvent.click(await screen.findByText('common.mainNav.workspace.inviteMembers'))
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.MEMBERS,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.MEMBERS)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'common.mainNav.workspace.openMenu' }))
|
||||
fireEvent.click(await screen.findByText('Evan Workspace'))
|
||||
@ -1168,9 +1165,7 @@ describe('MainNav', () => {
|
||||
expect(screen.queryByText('billing.upgradeBtn.encourageShort')).not.toBeInTheDocument()
|
||||
fireEvent.click(screen.getByText('billing.upgradeBtn.plain'))
|
||||
expect(mockSetShowPricingModal).toHaveBeenCalled()
|
||||
expect(mockSetShowAccountSettingModal).not.toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.BILLING,
|
||||
})
|
||||
expect(mockSetSettingsDestination).not.toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.BILLING)
|
||||
})
|
||||
|
||||
it('limits invite members by member management permission', async () => {
|
||||
|
||||
@ -110,7 +110,11 @@ const currentWorkspaceValue: PostWorkspacesCurrentResponse = {
|
||||
}
|
||||
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
let mockCurrentWorkspace: PostWorkspacesCurrentResponse | undefined = currentWorkspaceValue
|
||||
let mockWorkspaces: IWorkspace[] = []
|
||||
|
||||
@ -182,7 +186,6 @@ describe('WorkspaceCard', () => {
|
||||
mockWorkspacePermissionKeys(['workspace.member.manage'])
|
||||
vi.mocked(useModalContext).mockReturnValue({
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
} as unknown as ModalContextState)
|
||||
})
|
||||
|
||||
@ -448,9 +451,7 @@ describe('WorkspaceCard', () => {
|
||||
await screen.findByRole('button', { name: 'common.mainNav.workspace.settings' }),
|
||||
)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.BILLING,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.BILLING)
|
||||
})
|
||||
|
||||
it('opens members settings from workspace menu when billing is disabled', async () => {
|
||||
@ -466,12 +467,8 @@ describe('WorkspaceCard', () => {
|
||||
await screen.findByRole('button', { name: 'common.mainNav.workspace.settings' }),
|
||||
)
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.MEMBERS,
|
||||
})
|
||||
expect(mockSetShowAccountSettingModal).not.toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.BILLING,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.MEMBERS)
|
||||
expect(mockSetSettingsDestination).not.toHaveBeenCalledWith(ACCOUNT_SETTING_TAB.BILLING)
|
||||
})
|
||||
|
||||
it('switches workspace from the workspace switcher item', async () => {
|
||||
|
||||
@ -7,11 +7,15 @@ import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from '@langgeni
|
||||
import { toast } from '@langgenius/dify-ui/toast'
|
||||
import { useMutation, useQuery, useQueryClient, useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { WorkspaceAvatar } from '@/app/components/base/workspace-avatar'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import LicenseNav from '@/app/components/header/license-env'
|
||||
import { buildIntegrationPath } from '@/app/components/integrations/routes'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
@ -259,7 +263,8 @@ export function WorkspaceCard() {
|
||||
const currentWorkspace = currentWorkspaceQuery.data
|
||||
const workspaces = workspacesQuery.data?.workspaces
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const { setShowPricingModal, setShowAccountSettingModal } = useModalContext()
|
||||
const { setShowPricingModal } = useModalContext()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const isCloudEdition = deploymentEdition === 'CLOUD'
|
||||
const prefetchWorkspaces = () => {
|
||||
void queryClient.prefetchQuery(workspacesQueryOptions)
|
||||
@ -329,13 +334,11 @@ export function WorkspaceCard() {
|
||||
inviteMembersLabel={t(($) => $['mainNav.workspace.inviteMembers'], { ns: 'common' })}
|
||||
onOpenSettings={() => {
|
||||
setOpen(false)
|
||||
setShowAccountSettingModal({
|
||||
payload: hasBillingPlan ? ACCOUNT_SETTING_TAB.BILLING : ACCOUNT_SETTING_TAB.MEMBERS,
|
||||
})
|
||||
setSettingsDestination(hasBillingPlan ? 'billing' : 'members')
|
||||
}}
|
||||
onInviteMembers={() => {
|
||||
setOpen(false)
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })
|
||||
setSettingsDestination('members')
|
||||
}}
|
||||
/>
|
||||
<WorkspaceSwitcher
|
||||
|
||||
@ -1,12 +1,11 @@
|
||||
import { cleanup, fireEvent, screen } from '@testing-library/react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { render } from '@/test/console/render'
|
||||
import PluginAuth from '../plugin-auth'
|
||||
import { AuthCategory } from '../types'
|
||||
|
||||
const mockUsePluginAuth = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
const mockConsoleState = vi.hoisted(() => ({
|
||||
workspacePermissionKeys: ['credential.use', 'credential.create', 'credential.manage'] as string[],
|
||||
}))
|
||||
@ -31,11 +30,10 @@ vi.mock('@/context/permission-state', async () => {
|
||||
}))
|
||||
})
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
const defaultPayload = {
|
||||
category: AuthCategory.tool,
|
||||
@ -155,9 +153,7 @@ describe('PluginAuth', () => {
|
||||
render(<PluginAuth pluginPayload={defaultPayload} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.auth.permissionHint.action' }))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.MEMBERS,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('members')
|
||||
})
|
||||
|
||||
it('does not render permission hint for datasource authorization', () => {
|
||||
|
||||
@ -1,9 +1,12 @@
|
||||
import type { PluginPayload } from './types'
|
||||
import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { memo } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContext } from '@/context/modal-context'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { useCredentialPermissions } from '@/hooks/use-credential-permissions'
|
||||
import Authorize from './authorize'
|
||||
import Authorized from './authorized'
|
||||
@ -17,7 +20,7 @@ type PluginAuthProps = {
|
||||
}
|
||||
const PluginAuth = ({ pluginPayload, children, className }: PluginAuthProps) => {
|
||||
const { t } = useTranslation()
|
||||
const { setShowAccountSettingModal } = useModalContext()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const { canCreateCredential } = useCredentialPermissions()
|
||||
const {
|
||||
isAuthorized,
|
||||
@ -66,9 +69,7 @@ const PluginAuth = ({ pluginPayload, children, className }: PluginAuthProps) =>
|
||||
<button
|
||||
type="button"
|
||||
className="-ml-1.5 rounded-md px-1.5 py-0.5 system-xs-medium text-text-accent hover:bg-state-base-hover focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() =>
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.MEMBERS })
|
||||
}
|
||||
onClick={() => setSettingsDestination('members')}
|
||||
>
|
||||
{t(($) => $['auth.permissionHint.action'], { ns: 'plugin' })}
|
||||
</button>
|
||||
|
||||
@ -8,7 +8,6 @@ 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 { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { createAccountProfileQueryClient } from '@/test/console/account-profile'
|
||||
import { PluginCategoryEnum, PluginSource } from '../../../types'
|
||||
import AutoUpdateSetting from '../index'
|
||||
@ -34,17 +33,11 @@ dayjs.extend(timezone)
|
||||
// Mock app context
|
||||
const mockTimezone = 'America/New_York'
|
||||
|
||||
// Mock modal context
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContextSelector: (
|
||||
selector: (s: {
|
||||
setShowAccountSettingModal: typeof mockSetShowAccountSettingModal
|
||||
}) => typeof mockSetShowAccountSettingModal,
|
||||
) => {
|
||||
return selector({ setShowAccountSettingModal: mockSetShowAccountSettingModal })
|
||||
},
|
||||
}))
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
// Mock i18n context
|
||||
|
||||
@ -1396,9 +1389,7 @@ describe('auto-update-setting', () => {
|
||||
fireEvent.click(screen.getByText('autoUpdate.changeTimezone'))
|
||||
|
||||
// Assert
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: ACCOUNT_SETTING_TAB.PREFERENCES,
|
||||
})
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('preferences')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@ -6,13 +6,16 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { SegmentedControl, SegmentedControlItem } from '@langgenius/dify-ui/segmented-control'
|
||||
import { RiTimeLine } from '@remixicon/react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
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 { useModalContextSelector } from '@/context/modal-context'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import Label from '../label'
|
||||
import PluginsPicker from './plugins-picker'
|
||||
@ -35,12 +38,12 @@ type Props = Readonly<{
|
||||
const SettingTimeZone: FC<{
|
||||
children?: React.ReactNode
|
||||
}> = ({ children }) => {
|
||||
const setShowAccountSettingModal = useModalContextSelector((s) => s.setShowAccountSettingModal)
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="cursor-pointer border-none bg-transparent p-0 text-left body-xs-regular text-text-accent focus-visible:ring-1 focus-visible:ring-components-input-border-active focus-visible:outline-hidden"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
|
||||
onClick={() => setSettingsDestination('preferences')}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
|
||||
@ -14,6 +14,7 @@ import { Plan } from '@/app/components/billing/type'
|
||||
import { systemFeaturesQueryOptions } from '@/features/system-features/client'
|
||||
import { seedRegisteredConsoleStateFixture } from '@/test/console/state-fixture'
|
||||
import { createSystemFeaturesFixture } from '@/test/console/system-features'
|
||||
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||
import { createTestQueryClient } from '@/test/query-client'
|
||||
import StepByStepTourMount from '../mount'
|
||||
import { stepByStepTourSessionAtom } from '../state'
|
||||
@ -480,7 +481,7 @@ const setStepByStepTourTestState = (state: Partial<StepByStepTourFixtureState>)
|
||||
}
|
||||
}
|
||||
|
||||
const renderStepByStepTourMount = () => {
|
||||
const renderStepByStepTourMount = (searchParams = '') => {
|
||||
const queryClient = createTestQueryClient()
|
||||
queryClient.setQueryData(mockStepByStepTour.stateQueryKey, mockStepByStepTour.state)
|
||||
queryClient.setQueryData(
|
||||
@ -495,6 +496,7 @@ const renderStepByStepTourMount = () => {
|
||||
seedRegisteredConsoleStateFixture(jotaiStore)
|
||||
jotaiStore.set(queryClientAtom, queryClient)
|
||||
jotaiStore.set(stepByStepTourSessionAtom, mockStepByStepTour.uiState)
|
||||
const { wrapper } = createNuqsTestWrapper({ searchParams })
|
||||
|
||||
return render(
|
||||
<JotaiProvider store={jotaiStore}>
|
||||
@ -502,6 +504,7 @@ const renderStepByStepTourMount = () => {
|
||||
<StepByStepTourMount />
|
||||
</QueryClientProvider>
|
||||
</JotaiProvider>,
|
||||
{ wrapper },
|
||||
)
|
||||
}
|
||||
|
||||
@ -782,6 +785,23 @@ describe('StepByStepTourMount', () => {
|
||||
expect(document.body.querySelector('[data-base-ui-portal]')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('hides expanded tour overlays while settings is open', async () => {
|
||||
setStepByStepTourTestState({
|
||||
manuallyEnabledWorkspaceIds: ['workspace-1'],
|
||||
manuallyDisabledWorkspaceIds: [],
|
||||
minimized: false,
|
||||
completedTaskIds: [],
|
||||
skipped: false,
|
||||
})
|
||||
|
||||
renderStepByStepTourMount('?settings=preferences')
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('region', { name: 'Get to know Dify' })).not.toBeInTheDocument()
|
||||
})
|
||||
expect(document.body.querySelector('[data-base-ui-portal]')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('keeps the minimized tour entry available while a blocking modal is open', async () => {
|
||||
mockHasBlockingModalOpen.value = true
|
||||
localStorage.setItem(STEP_BY_STEP_TOUR_SHELL_MODE_STORAGE_KEY, 'collapsed')
|
||||
|
||||
@ -11,8 +11,13 @@ import { cn } from '@langgenius/dify-ui/cn'
|
||||
import { Popover, PopoverContent } from '@langgenius/dify-ui/popover'
|
||||
import { useSuspenseQuery } from '@tanstack/react-query'
|
||||
import { useAtomValue, useSetAtom } from 'jotai'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import { buildIntegrationPath } from '@/app/components/integrations/routes'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
@ -130,6 +135,7 @@ export default function StepByStepTourMount({ className }: StepByStepTourMountPr
|
||||
const isCurrentWorkspaceManager = useAtomValue(isCurrentWorkspaceManagerAtom)
|
||||
const workspacePermissionKeys = useAtomValue(workspacePermissionKeysAtom)
|
||||
const hasBlockingModalOpen = useModalContextSelector((state) => state.hasBlockingModalOpen)
|
||||
const [settingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions())
|
||||
const completedTaskIds = useAtomValue(completedStepByStepTourTaskIdsAtom)
|
||||
const skipped = useAtomValue(stepByStepTourSkippedAtom)
|
||||
@ -232,7 +238,7 @@ export default function StepByStepTourMount({ className }: StepByStepTourMountPr
|
||||
stepByStepTourFeatureEnabled &&
|
||||
enabledForCurrentWorkspace &&
|
||||
(hasActiveGuide || !shouldHideOnPathname(pathname))
|
||||
const overlayVisible = visible && !hasBlockingModalOpen
|
||||
const overlayVisible = visible && !hasBlockingModalOpen && !settingsDestination
|
||||
const completionPromptVisible = visible && allTasksCompleted && !activeTask
|
||||
const checklistMinimized = completionPromptVisible ? false : minimized
|
||||
const expanded = !checklistMinimized
|
||||
|
||||
@ -22,11 +22,9 @@ vi.mock('@/service/tools', () => ({
|
||||
const parseParamsSchemaMock = vi.mocked(parseParamsSchema)
|
||||
|
||||
const mockSetShowPricingModal = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: (): ModalContextState => ({
|
||||
hasBlockingModalOpen: false,
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
setShowModerationSettingModal: vi.fn(),
|
||||
setShowExternalDataToolModal: vi.fn(),
|
||||
setShowPricingModal: mockSetShowPricingModal,
|
||||
|
||||
@ -11,7 +11,7 @@ const mockHandleNodeDataUpdate = vi.fn()
|
||||
const mockHandleNodeDataUpdateWithSyncDraft = vi.fn()
|
||||
const mockSaveStateToHistory = vi.fn()
|
||||
const mockSetDetail = vi.fn()
|
||||
const mockSetShowAccountSettingModal = vi.fn()
|
||||
const mockSetSettingsDestination = vi.fn()
|
||||
const mockHandleSingleRun = vi.fn()
|
||||
const mockHandleStop = vi.fn()
|
||||
const mockHandleRunWithParams = vi.fn()
|
||||
@ -238,11 +238,10 @@ vi.mock('@/service/use-triggers', () => ({
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
vi.mock('nuqs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('nuqs')>()
|
||||
return { ...actual, useQueryState: () => [null, mockSetSettingsDestination] }
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/workflow/utils', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/app/components/workflow/utils')>()
|
||||
@ -672,7 +671,7 @@ describe('workflow-panel index', () => {
|
||||
|
||||
fireEvent.click(screen.getByText('authorized-in-datasource-node'))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalled()
|
||||
expect(mockSetSettingsDestination).toHaveBeenCalledWith('data-source')
|
||||
})
|
||||
|
||||
it('should react to pending single run actions', () => {
|
||||
|
||||
@ -7,6 +7,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@langgenius/dify-ui/too
|
||||
import { RiCloseLine, RiPlayLargeLine } from '@remixicon/react'
|
||||
import { debounce } from 'es-toolkit/compat'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useQueryState } from 'nuqs'
|
||||
import * as React from 'react'
|
||||
import { cloneElement, memo, useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
@ -14,9 +15,11 @@ import { useShallow } from 'zustand/react/shallow'
|
||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
|
||||
import { UserAvatarList } from '@/app/components/base/user-avatar-list'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
|
||||
import { useIntegrationsSetting } from '@/app/components/header/account-setting/use-integrations-setting'
|
||||
import {
|
||||
settingsQueryParamName,
|
||||
settingsQueryParser,
|
||||
} from '@/app/components/header/account-setting/query-params'
|
||||
import {
|
||||
AuthCategory,
|
||||
AuthorizedInDataSourceNode,
|
||||
@ -374,11 +377,11 @@ const BasePanel: FC<BasePanelProps> = ({ id, data, children }) => {
|
||||
[handleNodeDataUpdateWithSyncDraft, id],
|
||||
)
|
||||
|
||||
const openIntegrationsSetting = useIntegrationsSetting()
|
||||
const [, setSettingsDestination] = useQueryState(settingsQueryParamName, settingsQueryParser)
|
||||
|
||||
const handleJumpToDataSourcePage = useCallback(() => {
|
||||
openIntegrationsSetting({ payload: ACCOUNT_SETTING_TAB.DATA_SOURCE })
|
||||
}, [openIntegrationsSetting])
|
||||
setSettingsDestination('data-source')
|
||||
}, [setSettingsDestination])
|
||||
|
||||
const { appendNodeInspectVars } = useInspectVarsCrud()
|
||||
|
||||
|
||||
@ -66,10 +66,6 @@ vi.mock('@/hooks/use-async-window-open', () => ({
|
||||
useAsyncWindowOpen: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/education-apply/storage', () => ({
|
||||
useSetEducationVerifying: () => vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleClient: {
|
||||
billing: {
|
||||
|
||||
@ -1,2 +0,0 @@
|
||||
export const EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION = 'getEducationVerify'
|
||||
export const EDUCATION_RE_VERIFY_ACTION = 'educationReVerify'
|
||||
@ -13,7 +13,6 @@ import { useState } from 'react'
|
||||
import { Trans, useTranslation } from 'react-i18next'
|
||||
import { useEducationDiscount } from '@/app/components/billing/hooks/use-education-discount'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { useSetEducationVerifying } from '@/app/education-apply/storage'
|
||||
import { useDocLink } from '@/context/i18n'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { currentWorkspaceAtom, isCurrentWorkspaceManagerAtom } from '@/context/workspace-state'
|
||||
@ -51,7 +50,6 @@ const EducationApplyAgeContent = () => {
|
||||
const router = useRouter()
|
||||
const openAsyncWindow = useAsyncWindowOpen()
|
||||
const switchWorkspaceMutation = useMutation(consoleQuery.workspaces.switch.post.mutationOptions())
|
||||
const setEducationVerifying = useSetEducationVerifying()
|
||||
|
||||
const searchParams = useSearchParams()
|
||||
const token = searchParams.get('token')
|
||||
@ -71,7 +69,6 @@ const EducationApplyAgeContent = () => {
|
||||
if (res.message === 'success') {
|
||||
onPlanInfoChanged()
|
||||
updateEducationStatus()
|
||||
setEducationVerifying(null)
|
||||
setHasSubmittedEducation(true)
|
||||
} else {
|
||||
toast.error(t(($) => $.submitError, { ns: 'education' }))
|
||||
|
||||
@ -5,19 +5,15 @@ import dayjs from 'dayjs'
|
||||
import timezone from 'dayjs/plugin/timezone'
|
||||
import utc from 'dayjs/plugin/utc'
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
useEducationExpiredHasNoticed,
|
||||
useEducationReverifyHasNoticed,
|
||||
useEducationReverifyPrevExpireAt,
|
||||
useEducationVerifying,
|
||||
} from '@/app/education-apply/storage'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { userProfileQueryOptions } from '@/features/account-profile/client'
|
||||
import { useRouter, useSearchParams } from '@/next/navigation'
|
||||
import { useEducationAutocomplete, useEducationVerify } from '@/service/use-education'
|
||||
import { EDUCATION_RE_VERIFY_ACTION, EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION } from './constants'
|
||||
import { useEducationAutocomplete } from '@/service/use-education'
|
||||
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(timezone)
|
||||
@ -119,37 +115,13 @@ const useEducationReverifyNotice = ({ onNotice }: useEducationReverifyNoticePara
|
||||
}
|
||||
|
||||
export const useEducationInit = () => {
|
||||
const setShowAccountSettingModal = useModalContextSelector((s) => s.setShowAccountSettingModal)
|
||||
const setShowEducationExpireNoticeModal = useModalContextSelector(
|
||||
(s) => s.setShowEducationExpireNoticeModal,
|
||||
)
|
||||
const [educationVerifying, setEducationVerifying] = useEducationVerifying()
|
||||
const searchParams = useSearchParams()
|
||||
const educationVerifyAction = searchParams.get('action')
|
||||
|
||||
useEducationReverifyNotice({
|
||||
onNotice: (payload) => {
|
||||
setShowEducationExpireNoticeModal({ payload })
|
||||
},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
const { mutateAsync } = useEducationVerify()
|
||||
const handleVerify = async () => {
|
||||
const { token } = await mutateAsync()
|
||||
if (token) router.push(`/education-apply?token=${token}`)
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
educationVerifying === 'yes' ||
|
||||
educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION
|
||||
) {
|
||||
setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })
|
||||
|
||||
if (educationVerifyAction === EDUCATION_VERIFY_URL_SEARCHPARAMS_ACTION)
|
||||
setEducationVerifying('yes')
|
||||
}
|
||||
if (educationVerifyAction === EDUCATION_RE_VERIFY_ACTION) handleVerify()
|
||||
}, [setShowAccountSettingModal, setEducationVerifying, educationVerifying, educationVerifyAction])
|
||||
}
|
||||
|
||||
@ -1,10 +1,5 @@
|
||||
import { createLocalStorageState } from 'foxact/create-local-storage-state'
|
||||
|
||||
const EDUCATION_VERIFYING_LOCALSTORAGE_ITEM = 'educationVerifying'
|
||||
|
||||
const [useEducationVerifying, _useEducationVerifyingValue, useSetEducationVerifying] =
|
||||
createLocalStorageState<string>(EDUCATION_VERIFYING_LOCALSTORAGE_ITEM, 'no', { raw: true })
|
||||
|
||||
const [
|
||||
useEducationReverifyPrevExpireAt,
|
||||
_useEducationReverifyPrevExpireAtValue,
|
||||
@ -27,9 +22,7 @@ export {
|
||||
useEducationExpiredHasNoticed,
|
||||
useEducationReverifyHasNoticed,
|
||||
useEducationReverifyPrevExpireAt,
|
||||
useEducationVerifying,
|
||||
useSetEducationExpiredHasNoticed,
|
||||
useSetEducationReverifyHasNoticed,
|
||||
useSetEducationReverifyPrevExpireAt,
|
||||
useSetEducationVerifying,
|
||||
}
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
'use client'
|
||||
|
||||
import type { ReactNode, SetStateAction } from 'react'
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ModalState, ModelModalType } from './modal-context'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations'
|
||||
import type { SettingsTab } from '@/app/components/header/account-setting/constants'
|
||||
import type { ModelLoadBalancingModalProps } from '@/app/components/header/account-setting/model-provider-page/provider-added-card/model-load-balancing-modal'
|
||||
import type { UpdatePluginPayload } from '@/app/components/plugins/types'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
@ -12,31 +11,14 @@ import type { ExpireNoticeModalPayloadProps } from '@/app/education-apply/expire
|
||||
import type { ExternalDataTool } from '@/models/common'
|
||||
import type { ModerationConfig, PromptVariable } from '@/models/debug'
|
||||
import { useAtomValue } from 'jotai'
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import {
|
||||
DEFAULT_ACCOUNT_SETTING_TAB,
|
||||
isIntegrationSettingTab,
|
||||
isUserSettingTab,
|
||||
isValidSettingsTab,
|
||||
isWorkspaceSettingTab,
|
||||
} from '@/app/components/header/account-setting/constants'
|
||||
import { useSetEducationVerifying } from '@/app/education-apply/storage'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { useProviderContext } from '@/context/provider-context'
|
||||
import { currentWorkspaceIdAtom } from '@/context/workspace-state'
|
||||
import { useAccountSettingModal, usePricingModal } from '@/hooks/use-query-params'
|
||||
import { usePricingModal } from '@/hooks/use-query-params'
|
||||
import dynamic from '@/next/dynamic'
|
||||
import { useTriggerEventsLimitModal } from './hooks/use-trigger-events-limit-modal'
|
||||
import { ModalContext } from './modal-context'
|
||||
|
||||
const AccountSetting = dynamic(() => import('@/app/components/header/account-setting'), {
|
||||
ssr: false,
|
||||
})
|
||||
const IntegrationsSettingModal = dynamic(
|
||||
() => import('@/app/components/tools/integrations-setting-modal'),
|
||||
{
|
||||
ssr: false,
|
||||
},
|
||||
)
|
||||
const ModerationSettingModal = dynamic(
|
||||
() =>
|
||||
import('@/app/components/base/features/new-feature-panel/moderation/moderation-setting-modal'),
|
||||
@ -102,19 +84,7 @@ type ModalContextProviderProps = {
|
||||
children: ReactNode
|
||||
}
|
||||
export const ModalContextProvider = ({ children }: ModalContextProviderProps) => {
|
||||
// Use nuqs hooks for URL-based modal state management
|
||||
const [showPricingModal, setPricingModalOpen] = usePricingModal()
|
||||
const [urlAccountModalState, setUrlAccountModalState] = useAccountSettingModal()
|
||||
|
||||
const accountSettingCallbacksRef = useRef<Omit<ModalState<SettingsTab>, 'payload'> | null>(null)
|
||||
const settingsTab = urlAccountModalState.isOpen
|
||||
? isValidSettingsTab(urlAccountModalState.payload)
|
||||
? urlAccountModalState.payload
|
||||
: DEFAULT_ACCOUNT_SETTING_TAB
|
||||
: null
|
||||
const accountSettingModalTab =
|
||||
isWorkspaceSettingTab(settingsTab) || isUserSettingTab(settingsTab) ? settingsTab : null
|
||||
const integrationSettingModalSection = isIntegrationSettingTab(settingsTab) ? settingsTab : null
|
||||
const [showModerationSettingModal, setShowModerationSettingModal] =
|
||||
useState<ModalState<ModerationConfig> | null>(null)
|
||||
const [showExternalDataToolModal, setShowExternalDataToolModal] =
|
||||
@ -136,47 +106,8 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
|
||||
const [showEducationExpireNoticeModal, setShowEducationExpireNoticeModal] =
|
||||
useState<ModalState<ExpireNoticeModalPayloadProps> | null>(null)
|
||||
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom)
|
||||
const setEducationVerifying = useSetEducationVerifying()
|
||||
|
||||
const [showAnnotationFullModal, setShowAnnotationFullModal] = useState(false)
|
||||
const handleCancelAccountSettingModal = () => {
|
||||
setEducationVerifying((educationVerifying) =>
|
||||
educationVerifying === 'yes' ? null : educationVerifying,
|
||||
)
|
||||
accountSettingCallbacksRef.current?.onCancelCallback?.()
|
||||
accountSettingCallbacksRef.current = null
|
||||
setUrlAccountModalState(null)
|
||||
}
|
||||
|
||||
const handleAccountSettingTabChange = useCallback(
|
||||
(tab: SettingsTab) => {
|
||||
setUrlAccountModalState({ payload: tab })
|
||||
},
|
||||
[setUrlAccountModalState],
|
||||
)
|
||||
|
||||
const setShowAccountSettingModal = useCallback(
|
||||
(next: SetStateAction<ModalState<SettingsTab> | null>) => {
|
||||
const currentState = settingsTab
|
||||
? { payload: settingsTab, ...accountSettingCallbacksRef.current }
|
||||
: null
|
||||
const resolvedState = typeof next === 'function' ? next(currentState) : next
|
||||
if (!resolvedState) {
|
||||
accountSettingCallbacksRef.current = null
|
||||
setUrlAccountModalState(null)
|
||||
return
|
||||
}
|
||||
const { payload, ...callbacks } = resolvedState
|
||||
accountSettingCallbacksRef.current = callbacks
|
||||
setUrlAccountModalState({ payload })
|
||||
},
|
||||
[settingsTab, setUrlAccountModalState],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (!urlAccountModalState.isOpen) accountSettingCallbacksRef.current = null
|
||||
}, [urlAccountModalState.isOpen])
|
||||
|
||||
const { plan, isFetchedPlan } = useProviderContext()
|
||||
const {
|
||||
showTriggerEventsLimitModal,
|
||||
@ -281,8 +212,6 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
|
||||
setPricingModalOpen(false)
|
||||
}, [setPricingModalOpen])
|
||||
const hasBlockingModalOpen = Boolean(
|
||||
accountSettingModalTab ||
|
||||
integrationSettingModalSection ||
|
||||
showModerationSettingModal ||
|
||||
showExternalDataToolModal ||
|
||||
showPricingModal ||
|
||||
@ -300,7 +229,6 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
|
||||
<ModalContext.Provider
|
||||
value={{
|
||||
hasBlockingModalOpen,
|
||||
setShowAccountSettingModal,
|
||||
setShowModerationSettingModal,
|
||||
setShowExternalDataToolModal,
|
||||
setShowPricingModal: handleShowPricingModal,
|
||||
@ -316,22 +244,6 @@ export const ModalContextProvider = ({ children }: ModalContextProviderProps) =>
|
||||
>
|
||||
<>
|
||||
{children}
|
||||
{accountSettingModalTab && (
|
||||
<AccountSetting
|
||||
activeTab={accountSettingModalTab}
|
||||
onCancelAction={handleCancelAccountSettingModal}
|
||||
onTabChangeAction={handleAccountSettingTabChange}
|
||||
/>
|
||||
)}
|
||||
{integrationSettingModalSection && (
|
||||
<IntegrationsSettingModal
|
||||
section={integrationSettingModalSection}
|
||||
source={accountSettingCallbacksRef.current?.source}
|
||||
onCancel={handleCancelAccountSettingModal}
|
||||
onSectionChange={(section) => setUrlAccountModalState({ payload: section })}
|
||||
/>
|
||||
)}
|
||||
|
||||
{!!showModerationSettingModal && (
|
||||
<ModerationSettingModal
|
||||
data={showModerationSettingModal.payload}
|
||||
|
||||
@ -3,15 +3,11 @@ import userEvent from '@testing-library/user-event'
|
||||
import * as React from 'react'
|
||||
import { defaultPlan } from '@/app/components/billing/config'
|
||||
import { Plan } from '@/app/components/billing/type'
|
||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||
import { useModalContextSelector } from '@/context/modal-context'
|
||||
import { ModalContextProvider } from '@/context/modal-context-provider'
|
||||
import { createConsoleQueryWrapper } from '@/test/console/query-data'
|
||||
import { render } from '@/test/console/render'
|
||||
import { createNuqsTestWrapper } from '@/test/nuqs-testing'
|
||||
|
||||
const mockSetEducationVerifying = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/next/navigation', () => ({
|
||||
useRouter: () => ({
|
||||
push: vi.fn(),
|
||||
@ -23,23 +19,6 @@ vi.mock('@/app/components/billing/pricing', () => ({
|
||||
default: () => <div>billing.plansCommon.mostPopular</div>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting', () => ({
|
||||
default: ({ activeTab, onCancelAction }: { activeTab: string; onCancelAction: () => void }) => (
|
||||
<>
|
||||
<div role="status" aria-label="active account setting tab">
|
||||
{activeTab}
|
||||
</div>
|
||||
<button type="button" onClick={onCancelAction}>
|
||||
cancel account setting
|
||||
</button>
|
||||
</>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/education-apply/storage', () => ({
|
||||
useSetEducationVerifying: () => mockSetEducationVerifying,
|
||||
}))
|
||||
|
||||
const mockUseProviderContext = vi.fn()
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => mockUseProviderContext(),
|
||||
@ -97,47 +76,10 @@ const renderProvider = (
|
||||
return render(<ModalContextProvider>{children}</ModalContextProvider>, { wrapper })
|
||||
}
|
||||
|
||||
const AccountSettingOpener = () => {
|
||||
const setShowAccountSettingModal = useModalContextSelector(
|
||||
(state) => state.setShowAccountSettingModal,
|
||||
)
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.BILLING })}
|
||||
>
|
||||
open account setting
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const PreferencesOpener = () => {
|
||||
const setShowAccountSettingModal = useModalContextSelector(
|
||||
(state) => state.setShowAccountSettingModal,
|
||||
)
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowAccountSettingModal({ payload: ACCOUNT_SETTING_TAB.PREFERENCES })}
|
||||
>
|
||||
open preferences
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
const BlockingModalProbe = () => {
|
||||
const hasBlockingModalOpen = useModalContextSelector((state) => state.hasBlockingModalOpen)
|
||||
|
||||
return <div data-testid="has-blocking-modal-open">{String(hasBlockingModalOpen)}</div>
|
||||
}
|
||||
|
||||
describe('ModalContextProvider trigger events limit modal', () => {
|
||||
beforeEach(() => {
|
||||
mockConsoleStateReader.mockReset()
|
||||
mockUseProviderContext.mockReset()
|
||||
mockSetEducationVerifying.mockReset()
|
||||
window.localStorage.clear()
|
||||
mockConsoleStateReader.mockReturnValue({
|
||||
currentWorkspace: {
|
||||
@ -182,56 +124,6 @@ describe('ModalContextProvider trigger events limit modal', () => {
|
||||
expect(value).toBe('1')
|
||||
})
|
||||
|
||||
it('clears the education verifying flag when account settings are canceled', async () => {
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan: createPlan(),
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderProvider(<AccountSettingOpener />)
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'open account setting' }))
|
||||
await user.click(await screen.findByRole('button', { name: 'cancel account setting' }))
|
||||
|
||||
expect(mockSetEducationVerifying).toHaveBeenCalledWith(expect.any(Function))
|
||||
const updater = mockSetEducationVerifying.mock.calls[0]?.[0] as (
|
||||
educationVerifying: string,
|
||||
) => string | null
|
||||
expect(updater('yes')).toBeNull()
|
||||
expect(updater('no')).toBe('no')
|
||||
})
|
||||
|
||||
it('opens preferences in the account settings shell', async () => {
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
plan: createPlan(),
|
||||
isFetchedPlan: true,
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
|
||||
renderProvider(
|
||||
<>
|
||||
<BlockingModalProbe />
|
||||
<PreferencesOpener />
|
||||
</>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('has-blocking-modal-open')).toHaveTextContent('false')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'open preferences' }))
|
||||
|
||||
expect(
|
||||
await screen.findByRole('status', { name: 'active account setting tab' }),
|
||||
).toHaveTextContent(ACCOUNT_SETTING_TAB.PREFERENCES)
|
||||
expect(screen.getByTestId('has-blocking-modal-open')).toHaveTextContent('true')
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'cancel account setting' }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('has-blocking-modal-open')).toHaveTextContent('false')
|
||||
})
|
||||
})
|
||||
|
||||
it('relies on the in-memory guard when localStorage reads throw', async () => {
|
||||
const plan = createPlan({
|
||||
type: Plan.professional,
|
||||
|
||||
@ -4,7 +4,6 @@ import type { Dispatch, SetStateAction } from 'react'
|
||||
import type { TriggerEventsLimitModalPayload } from './hooks/use-trigger-events-limit-modal'
|
||||
import type { OpeningStatement } from '@/app/components/base/features/types'
|
||||
import type { CreateExternalAPIReq } from '@/app/components/datasets/external-api/declarations'
|
||||
import type { SettingsTab } from '@/app/components/header/account-setting/constants'
|
||||
import type {
|
||||
ConfigurationMethodEnum,
|
||||
Credential,
|
||||
@ -24,7 +23,6 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec
|
||||
|
||||
export type ModalState<T> = {
|
||||
payload: T
|
||||
source?: 'agent'
|
||||
onCancelCallback?: () => void
|
||||
onSaveCallback?: (newPayload?: T, formValues?: Record<string, unknown>) => void
|
||||
onRemoveCallback?: (newPayload?: T, formValues?: Record<string, unknown>) => void
|
||||
@ -46,7 +44,6 @@ export type ModelModalType = {
|
||||
|
||||
export type ModalContextState = {
|
||||
hasBlockingModalOpen: boolean
|
||||
setShowAccountSettingModal: Dispatch<SetStateAction<ModalState<SettingsTab> | null>>
|
||||
setShowModerationSettingModal: Dispatch<SetStateAction<ModalState<ModerationConfig> | null>>
|
||||
setShowExternalDataToolModal: Dispatch<SetStateAction<ModalState<ExternalDataTool> | null>>
|
||||
setShowPricingModal: () => void
|
||||
@ -76,7 +73,6 @@ export type ModalContextState = {
|
||||
|
||||
export const ModalContext = createContext<ModalContextState>({
|
||||
hasBlockingModalOpen: false,
|
||||
setShowAccountSettingModal: noop,
|
||||
setShowModerationSettingModal: noop,
|
||||
setShowExternalDataToolModal: noop,
|
||||
setShowPricingModal: noop,
|
||||
|
||||
@ -46,7 +46,6 @@ export function AgentModelField({
|
||||
modelList={textGenerationModelList}
|
||||
triggerClassName="h-8! w-full rounded-r-none! [&_.i-ri-arrow-down-s-line]:hidden"
|
||||
popupClassName="w-(--anchor-width) max-w-[min(var(--anchor-width),var(--available-width),calc(100vw-32px))]"
|
||||
providerSettingsSource="agent"
|
||||
showModelMeta={false}
|
||||
modelPredicate={isAgentCompatibleModel}
|
||||
modelSuggestionPredicate={isAgentSuggestedModel}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { act, waitFor } from '@testing-library/react'
|
||||
import { ACCOUNT_SETTING_MODAL_ACTION } from '@/app/components/header/account-setting/constants'
|
||||
import { renderHookWithNuqs } from '@/test/nuqs-testing'
|
||||
import {
|
||||
PRICING_MODAL_QUERY_PARAM,
|
||||
PRICING_MODAL_QUERY_VALUE,
|
||||
useAccountSettingModal,
|
||||
usePluginInstallation,
|
||||
usePricingModal,
|
||||
} from './use-query-params'
|
||||
@ -142,186 +140,6 @@ describe('useQueryParams hooks', () => {
|
||||
})
|
||||
})
|
||||
|
||||
// Account settings modal query behavior.
|
||||
describe('useAccountSettingModal', () => {
|
||||
it('should return closed state with null payload when query params are missing', () => {
|
||||
// Arrange
|
||||
const { result } = renderWithAdapter(() => useAccountSettingModal())
|
||||
|
||||
// Act
|
||||
const [state] = result.current
|
||||
|
||||
// Assert
|
||||
expect(state.isOpen).toBe(false)
|
||||
expect(state.payload).toBeNull()
|
||||
})
|
||||
|
||||
it('should return open state when action matches', () => {
|
||||
// Arrange
|
||||
const { result } = renderWithAdapter(
|
||||
() => useAccountSettingModal(),
|
||||
`?action=${ACCOUNT_SETTING_MODAL_ACTION}&tab=billing`,
|
||||
)
|
||||
|
||||
// Act
|
||||
const [state] = result.current
|
||||
|
||||
// Assert
|
||||
expect(state.isOpen).toBe(true)
|
||||
expect(state.payload).toBe('billing')
|
||||
})
|
||||
|
||||
it('should accept integrations tabs with the shared settings action', () => {
|
||||
// Arrange
|
||||
const { result } = renderWithAdapter(
|
||||
() => useAccountSettingModal(),
|
||||
`?action=${ACCOUNT_SETTING_MODAL_ACTION}&tab=mcp`,
|
||||
)
|
||||
|
||||
// Act
|
||||
const [state] = result.current
|
||||
|
||||
// Assert
|
||||
expect(state.isOpen).toBe(true)
|
||||
expect(state.payload).toBe('mcp')
|
||||
})
|
||||
|
||||
it('should return closed state when action does not match', () => {
|
||||
// Arrange
|
||||
const { result } = renderWithAdapter(
|
||||
() => useAccountSettingModal(),
|
||||
'?action=other&tab=billing',
|
||||
)
|
||||
|
||||
// Act
|
||||
const [state] = result.current
|
||||
|
||||
// Assert
|
||||
expect(state.isOpen).toBe(false)
|
||||
expect(state.payload).toBeNull()
|
||||
})
|
||||
|
||||
it('should set action and tab when opening', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(() => useAccountSettingModal())
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ payload: 'members' })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0]
|
||||
expect(update.searchParams.get('action')).toBe(ACCOUNT_SETTING_MODAL_ACTION)
|
||||
expect(update.searchParams.get('tab')).toBe('members')
|
||||
})
|
||||
|
||||
it('should set an integrations tab with the shared settings action', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(() => useAccountSettingModal())
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ payload: 'data-source' })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0]
|
||||
expect(update.searchParams.get('action')).toBe(ACCOUNT_SETTING_MODAL_ACTION)
|
||||
expect(update.searchParams.get('tab')).toBe('data-source')
|
||||
})
|
||||
|
||||
it('should use push history when opening from closed state', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(() => useAccountSettingModal())
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ payload: 'members' })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0]
|
||||
expect(update.options.history).toBe('push')
|
||||
})
|
||||
|
||||
it('should update tab when switching while open', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(
|
||||
() => useAccountSettingModal(),
|
||||
`?action=${ACCOUNT_SETTING_MODAL_ACTION}&tab=billing`,
|
||||
)
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ payload: 'provider' })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0]
|
||||
expect(update.searchParams.get('tab')).toBe('provider')
|
||||
})
|
||||
|
||||
it('should use replace history when switching tabs while open', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(
|
||||
() => useAccountSettingModal(),
|
||||
`?action=${ACCOUNT_SETTING_MODAL_ACTION}&tab=billing`,
|
||||
)
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1]({ payload: 'provider' })
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0]
|
||||
expect(update.options.history).toBe('replace')
|
||||
})
|
||||
|
||||
it('should clear action and tab when closing', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(
|
||||
() => useAccountSettingModal(),
|
||||
`?action=${ACCOUNT_SETTING_MODAL_ACTION}&tab=billing`,
|
||||
)
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1](null)
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0]
|
||||
expect(update.searchParams.has('action')).toBe(false)
|
||||
expect(update.searchParams.has('tab')).toBe(false)
|
||||
})
|
||||
|
||||
it('should use replace history when closing', async () => {
|
||||
// Arrange
|
||||
const { result, onUrlUpdate } = renderWithAdapter(
|
||||
() => useAccountSettingModal(),
|
||||
`?action=${ACCOUNT_SETTING_MODAL_ACTION}&tab=billing`,
|
||||
)
|
||||
|
||||
// Act
|
||||
act(() => {
|
||||
result.current[1](null)
|
||||
})
|
||||
|
||||
// Assert
|
||||
await waitFor(() => expect(onUrlUpdate).toHaveBeenCalled())
|
||||
const update = onUrlUpdate.mock.calls[onUrlUpdate.mock.calls.length - 1]![0]
|
||||
expect(update.options.history).toBe('replace')
|
||||
})
|
||||
})
|
||||
|
||||
// Plugin installation query behavior.
|
||||
describe('usePluginInstallation', () => {
|
||||
it('should parse package ids from JSON arrays', () => {
|
||||
|
||||
@ -13,19 +13,7 @@
|
||||
* - Use shallow routing to avoid unnecessary re-renders
|
||||
*/
|
||||
|
||||
import type { SettingsTab } from '@/app/components/header/account-setting/constants'
|
||||
import {
|
||||
createParser,
|
||||
parseAsStringEnum,
|
||||
parseAsStringLiteral,
|
||||
useQueryState,
|
||||
useQueryStates,
|
||||
} from 'nuqs'
|
||||
import { useCallback } from 'react'
|
||||
import {
|
||||
ACCOUNT_SETTING_MODAL_ACTION,
|
||||
SETTINGS_TAB_VALUES,
|
||||
} from '@/app/components/header/account-setting/constants'
|
||||
import { createParser, useQueryState, useQueryStates } from 'nuqs'
|
||||
|
||||
/**
|
||||
* Modal State Query Parameters
|
||||
@ -53,51 +41,6 @@ export function usePricingModal() {
|
||||
return useQueryState(PRICING_MODAL_QUERY_PARAM, parseAsPricingModal)
|
||||
}
|
||||
|
||||
const settingsTabValues = [...SETTINGS_TAB_VALUES] as SettingsTab[]
|
||||
const parseAsAccountSettingAction = parseAsStringLiteral([ACCOUNT_SETTING_MODAL_ACTION] as const)
|
||||
const parseAsSettingsTab = parseAsStringEnum<SettingsTab>(settingsTabValues)
|
||||
|
||||
/**
|
||||
* Hook to manage account setting modal state via URL
|
||||
* @returns [state, setState] - Object with isOpen + payload (tab) and setter
|
||||
*
|
||||
* @example
|
||||
* const [accountModalState, setAccountModalState] = useAccountSettingModal()
|
||||
* setAccountModalState({ payload: 'billing' }) // Sets ?action=showSettings&tab=billing
|
||||
* setAccountModalState(null) // Removes both params
|
||||
*/
|
||||
export function useAccountSettingModal() {
|
||||
const [accountState, setAccountState] = useQueryStates(
|
||||
{
|
||||
action: parseAsAccountSettingAction,
|
||||
tab: parseAsSettingsTab,
|
||||
},
|
||||
{
|
||||
history: 'replace',
|
||||
},
|
||||
)
|
||||
|
||||
const setState = useCallback(
|
||||
(state: { payload: SettingsTab } | null) => {
|
||||
if (!state) {
|
||||
setAccountState({ action: null, tab: null }, { history: 'replace' })
|
||||
return
|
||||
}
|
||||
const shouldPush = accountState.action !== ACCOUNT_SETTING_MODAL_ACTION
|
||||
setAccountState(
|
||||
{ action: ACCOUNT_SETTING_MODAL_ACTION, tab: state.payload },
|
||||
{ history: shouldPush ? 'push' : 'replace' },
|
||||
)
|
||||
},
|
||||
[accountState.action, setAccountState],
|
||||
)
|
||||
|
||||
const isOpen = accountState.action === ACCOUNT_SETTING_MODAL_ACTION
|
||||
const currentTab = isOpen ? accountState.tab : null
|
||||
|
||||
return [{ isOpen, payload: currentTab }, setState] as const
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Installation Query Parameters
|
||||
*/
|
||||
|
||||
Loading…
Reference in New Issue
Block a user