From 61e2672b598b1acfbc8d3a474208ab4a6ce490ff Mon Sep 17 00:00:00 2001 From: yyh Date: Thu, 5 Mar 2026 13:28:30 +0800 Subject: [PATCH] refactor(web): make provider reset event-driven and scope model invalidation - remove provider-page lifecycle reset effect and handle reset in explicit tab/close actions - switch account setting tab state to controlled/uncontrolled pattern without sync effect - use provider-scoped model list queryKey with exact invalidation in credential and model toggle mutations - update related tests and mocks for new behavior --- .../header/account-setting/index.spec.tsx | 13 ++++++- .../header/account-setting/index.tsx | 37 ++++++++++++++----- .../model-provider-page/index.spec.tsx | 5 --- .../model-provider-page/index.tsx | 9 +---- .../credential-panel.spec.tsx | 4 +- .../provider-added-card/credential-panel.tsx | 10 ++++- .../provider-added-card/model-list-item.tsx | 14 +++++-- web/eslint-suppressions.json | 17 --------- 8 files changed, 62 insertions(+), 47 deletions(-) diff --git a/web/app/components/header/account-setting/index.spec.tsx b/web/app/components/header/account-setting/index.spec.tsx index 3a98d8afb8..74480f0a1b 100644 --- a/web/app/components/header/account-setting/index.spec.tsx +++ b/web/app/components/header/account-setting/index.spec.tsx @@ -7,6 +7,8 @@ import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints' import { ACCOUNT_SETTING_TAB } from './constants' import AccountSetting from './index' +const mockResetModelProviderListExpanded = vi.fn() + vi.mock('@/context/provider-context', async (importOriginal) => { const actual = await importOriginal() return { @@ -47,10 +49,15 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () useDefaultModel: vi.fn(() => ({ data: null, isLoading: false })), useUpdateDefaultModel: vi.fn(() => ({ trigger: vi.fn() })), useUpdateModelList: vi.fn(() => vi.fn()), + useInvalidateDefaultModel: vi.fn(() => vi.fn()), useModelList: vi.fn(() => ({ data: [], isLoading: false })), useSystemDefaultModelAndModelList: vi.fn(() => [null, vi.fn()]), })) +vi.mock('@/app/components/header/account-setting/model-provider-page/atoms', () => ({ + useResetModelProviderListExpanded: () => mockResetModelProviderListExpanded, +})) + vi.mock('@/service/use-datasource', () => ({ useGetDataSourceListAuth: vi.fn(() => ({ data: { result: [] } })), })) @@ -272,8 +279,10 @@ describe('AccountSetting', () => { , ) - const buttons = screen.getAllByRole('button') - fireEvent.click(buttons[0]) + const closeIcon = document.querySelector('.i-ri-close-line') + const closeButton = closeIcon?.closest('button') + expect(closeButton).not.toBeNull() + fireEvent.click(closeButton!) // Assert expect(mockOnCancel).toHaveBeenCalled() diff --git a/web/app/components/header/account-setting/index.tsx b/web/app/components/header/account-setting/index.tsx index 45d8dde8a6..f9c91b7d95 100644 --- a/web/app/components/header/account-setting/index.tsx +++ b/web/app/components/header/account-setting/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { AccountSettingTab } from '@/app/components/header/account-setting/constants' -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import SearchInput from '@/app/components/base/search-input' import BillingPage from '@/app/components/billing/billing-page' @@ -20,6 +20,7 @@ import DataSourcePage from './data-source-page-new' import LanguagePage from './language-page' import MembersPage from './members-page' import ModelProviderPage from './model-provider-page' +import { useResetModelProviderListExpanded } from './model-provider-page/atoms' const iconClassName = ` w-5 h-5 mr-2 @@ -41,13 +42,15 @@ type GroupItem = { export default function AccountSetting({ onCancel, - activeTab = ACCOUNT_SETTING_TAB.MEMBERS, + activeTab, onTabChange, }: IAccountSettingProps) { - const [activeMenu, setActiveMenu] = useState(activeTab) - useEffect(() => { - setActiveMenu(activeTab) - }, [activeTab]) + const resetModelProviderListExpanded = useResetModelProviderListExpanded() + const isControlledTab = activeTab !== undefined && !!onTabChange + const [uncontrolledActiveMenu, setUncontrolledActiveMenu] = useState(activeTab ?? ACCOUNT_SETTING_TAB.MEMBERS) + const activeMenu = isControlledTab + ? (activeTab ?? ACCOUNT_SETTING_TAB.MEMBERS) + : uncontrolledActiveMenu const { t } = useTranslation() const { enableBilling, enableReplaceWebAppLogo } = useProviderContext() const { isCurrentWorkspaceDatasetOperator } = useAppContext() @@ -148,10 +151,25 @@ export default function AccountSetting({ const [searchValue, setSearchValue] = useState('') + const handleTabChange = useCallback((tab: AccountSettingTab) => { + if (tab === ACCOUNT_SETTING_TAB.PROVIDER) + resetModelProviderListExpanded() + + if (!isControlledTab) + setUncontrolledActiveMenu(tab) + + onTabChange?.(tab) + }, [isControlledTab, onTabChange, resetModelProviderListExpanded]) + + const handleClose = useCallback(() => { + resetModelProviderListExpanded() + onCancel() + }, [onCancel, resetModelProviderListExpanded]) + return (
@@ -174,8 +192,7 @@ export default function AccountSetting({ )} title={item.name} onClick={() => { - setActiveMenu(item.key) - onTabChange?.(item.key) + handleTabChange(item.key) }} > {activeMenu === item.key ? item.activeIcon : item.icon} @@ -195,7 +212,7 @@ export default function AccountSetting({ variant="tertiary" size="large" className="px-2" - onClick={onCancel} + onClick={handleClose} > diff --git a/web/app/components/header/account-setting/model-provider-page/index.spec.tsx b/web/app/components/header/account-setting/model-provider-page/index.spec.tsx index a41569a6e6..27cead7eb2 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.spec.tsx @@ -8,7 +8,6 @@ import { import ModelProviderPage from './index' let mockEnableMarketplace = true -const mockResetModelProviderListExpanded = vi.fn() const mockQuotaConfig = { quota_type: CurrentSystemQuotaTypeEnum.free, @@ -68,10 +67,6 @@ vi.mock('./hooks', () => ({ useDefaultModel: (type: string) => mockDefaultModels[type] ?? { data: null, isLoading: false }, })) -vi.mock('./atoms', () => ({ - useResetModelProviderListExpanded: () => mockResetModelProviderListExpanded, -})) - vi.mock('./install-from-marketplace', () => ({ default: () =>
, })) diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 6edce6ac09..a0d47d44ac 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -3,14 +3,13 @@ import type { } from './declarations' import type { PluginDetail } from '@/app/components/plugins/types' import { useDebounce } from 'ahooks' -import { useEffect, useMemo } from 'react' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { IS_CLOUD_EDITION } from '@/config' import { useSystemFeaturesQuery } from '@/context/global-public-context' import { useProviderContext } from '@/context/provider-context' import { useCheckInstalled } from '@/service/use-plugins' import { cn } from '@/utils/classnames' -import { useResetModelProviderListExpanded } from './atoms' import { CustomConfigurationStatusEnum, ModelTypeEnum, @@ -35,7 +34,6 @@ const FixedModelProvider = ['langgenius/openai/openai', 'langgenius/anthropic/an const ModelProviderPage = ({ searchText }: Props) => { const debouncedSearchText = useDebounce(searchText, { wait: 500 }) const { t } = useTranslation() - const resetModelProviderListExpanded = useResetModelProviderListExpanded() const { data: textGenerationDefaultModel, isLoading: isTextGenerationDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textGeneration) const { data: embeddingsDefaultModel, isLoading: isEmbeddingsDefaultModelLoading } = useDefaultModel(ModelTypeEnum.textEmbedding) const { data: rerankDefaultModel, isLoading: isRerankDefaultModelLoading } = useDefaultModel(ModelTypeEnum.rerank) @@ -129,11 +127,6 @@ const ModelProviderPage = ({ searchText }: Props) => { return [filteredConfiguredProviders, filteredNotConfiguredProviders] }, [configuredProviders, debouncedSearchText, notConfiguredProviders]) - useEffect(() => { - resetModelProviderListExpanded() - return resetModelProviderListExpanded - }, [resetModelProviderListExpanded]) - return (
diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.spec.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.spec.tsx index 5167ced3e1..e4a0e409f7 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.spec.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.spec.tsx @@ -35,7 +35,9 @@ vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/service/client', () => ({ consoleQuery: { modelProviders: { - models: { key: () => ['console', 'modelProviders', 'models'] }, + models: { + queryKey: ({ input }: { input: { params: { provider: string } } }) => ['console', 'modelProviders', 'models', input.params.provider], + }, changePreferredProviderType: { mutationOptions: (opts: Record) => ({ mutationFn: (...args: unknown[]) => { diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx index 89808e682b..a4d40b052f 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx @@ -41,13 +41,21 @@ const CredentialPanel = ({ const updateModelList = useUpdateModelList() const updateModelProviders = useUpdateModelProviders() const state = useCredentialPanelState(provider) + const modelProviderModelListQueryKey = consoleQuery.modelProviders.models.queryKey({ + input: { + params: { + provider: provider.provider, + }, + }, + }) const { mutate: changePriority, isPending: isChangingPriority } = useMutation( consoleQuery.modelProviders.changePreferredProviderType.mutationOptions({ onSuccess: () => { Toast.notify({ type: 'success', message: t('actionMsg.modifiedSuccessfully', { ns: 'common' }) }) queryClient.invalidateQueries({ - queryKey: consoleQuery.modelProviders.models.key(), + queryKey: modelProviderModelListQueryKey, + exact: true, refetchType: 'none', }) updateModelProviders() diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx index a21ece2384..595525d014 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/model-list-item.tsx @@ -34,6 +34,13 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad const { isCurrentWorkspaceManager } = useAppContext() const queryClient = useQueryClient() const updateModelList = useUpdateModelList() + const modelProviderModelListQueryKey = consoleQuery.modelProviders.models.queryKey({ + input: { + params: { + provider: provider.provider, + }, + }, + }) const toggleModelEnablingStatus = useCallback(async (enabled: boolean) => { if (enabled) @@ -42,12 +49,13 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad await disableModel(`/workspaces/current/model-providers/${provider.provider}/models/disable`, { model: model.model, model_type: model.model_type }) queryClient.invalidateQueries({ - queryKey: consoleQuery.modelProviders.models.key(), + queryKey: modelProviderModelListQueryKey, + exact: true, refetchType: 'none', }) updateModelList(model.model_type) onChange?.(provider.provider) - }, [model.model, model.model_type, onChange, provider.provider, queryClient, updateModelList]) + }, [model.model, model.model_type, modelProviderModelListQueryKey, onChange, provider.provider, queryClient, updateModelList]) const { run: debouncedToggleModelEnablingStatus } = useDebounceFn(toggleModelEnablingStatus, { wait: 500 }) @@ -66,7 +74,7 @@ const ModelListItem = ({ model, provider, isConfigurable, onChange, onModifyLoad modelName={model.model} />