From 72b96ba972e1eb81819752c96384ac7607e62df9 Mon Sep 17 00:00:00 2001 From: yyh Date: Mon, 9 Feb 2026 18:32:15 +0800 Subject: [PATCH] refactor(web): remove mutateCurrentWorkspace from AppContext, use service-layer invalidation hook The previous implementation stored query invalidation logic (mutateCurrentWorkspace) and isFetching state (isValidatingCurrentWorkspace) in React Context, causing QuotaPanel to flash a loading spinner on every settings tab switch due to a useEffect calling invalidateQueries on mount. This violated separation of concerns and React best practices. - Remove mutateCurrentWorkspace and isValidatingCurrentWorkspace from AppContext - Add useInvalidateCurrentWorkspace hook in service layer (consistent with project pattern) - Remove redundant useEffect + invalidateQueries in ModelProviderPage - QuotaPanel now derives loading from !currentWorkspace.id instead of external prop - Update custom-web-app-brand to use the new service-layer hook --- .../apps-full-in-dialog/index.spec.tsx | 2 -- .../custom-web-app-brand/index.spec.tsx | 14 ++++---- .../custom/custom-web-app-brand/index.tsx | 35 ++++++++++--------- .../model-provider-page/index.tsx | 20 ++++------- .../provider-added-card/quota-panel.tsx | 8 ++--- web/context/app-context.tsx | 12 +------ web/eslint-suppressions.json | 16 --------- web/service/use-common.ts | 4 +++ 8 files changed, 41 insertions(+), 70 deletions(-) diff --git a/web/app/components/billing/apps-full-in-dialog/index.spec.tsx b/web/app/components/billing/apps-full-in-dialog/index.spec.tsx index d006a3222d..135433e840 100644 --- a/web/app/components/billing/apps-full-in-dialog/index.spec.tsx +++ b/web/app/components/billing/apps-full-in-dialog/index.spec.tsx @@ -96,10 +96,8 @@ const buildAppContext = (overrides: Partial = {}): AppContextVa isCurrentWorkspaceEditor: false, isCurrentWorkspaceDatasetOperator: false, mutateUserProfile: vi.fn(), - mutateCurrentWorkspace: vi.fn(), langGeniusVersionInfo, isLoadingCurrentWorkspace: false, - isValidatingCurrentWorkspace: false, } const useSelector: AppContextValue['useSelector'] = selector => selector({ ...base, useSelector }) return { diff --git a/web/app/components/custom/custom-web-app-brand/index.spec.tsx b/web/app/components/custom/custom-web-app-brand/index.spec.tsx index e50ca4e9b2..5721db4487 100644 --- a/web/app/components/custom/custom-web-app-brand/index.spec.tsx +++ b/web/app/components/custom/custom-web-app-brand/index.spec.tsx @@ -7,6 +7,7 @@ import { useAppContext } from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context' import { useProviderContext } from '@/context/provider-context' import { updateCurrentWorkspace } from '@/service/common' +import { useInvalidateCurrentWorkspace } from '@/service/use-common' import CustomWebAppBrand from './index' vi.mock('@/app/components/base/toast', () => ({ @@ -15,6 +16,9 @@ vi.mock('@/app/components/base/toast', () => ({ vi.mock('@/service/common', () => ({ updateCurrentWorkspace: vi.fn(), })) +vi.mock('@/service/use-common', () => ({ + useInvalidateCurrentWorkspace: vi.fn(), +})) vi.mock('@/context/app-context', () => ({ useAppContext: vi.fn(), })) @@ -37,6 +41,8 @@ const mockUseProviderContext = vi.mocked(useProviderContext) const mockUseGlobalPublicStore = vi.mocked(useGlobalPublicStore) const mockImageUpload = vi.mocked(imageUpload) const mockGetImageUploadErrorMessage = vi.mocked(getImageUploadErrorMessage) +const mockInvalidateCurrentWorkspace = vi.fn() +vi.mocked(useInvalidateCurrentWorkspace).mockReturnValue(mockInvalidateCurrentWorkspace) const defaultPlanUsage = { buildApps: 0, @@ -62,7 +68,6 @@ describe('CustomWebAppBrand', () => { remove_webapp_brand: false, }, }, - mutateCurrentWorkspace: vi.fn(), isCurrentWorkspaceManager: true, } as any) mockUseProviderContext.mockReturnValue({ @@ -92,7 +97,6 @@ describe('CustomWebAppBrand', () => { remove_webapp_brand: false, }, }, - mutateCurrentWorkspace: vi.fn(), isCurrentWorkspaceManager: false, } as any) @@ -101,8 +105,7 @@ describe('CustomWebAppBrand', () => { expect(fileInput).toBeDisabled() }) - it('toggles remove brand switch and calls the backend + mutate', async () => { - const mutateMock = vi.fn() + it('toggles remove brand switch and calls the backend + invalidate', async () => { mockUseAppContext.mockReturnValue({ currentWorkspace: { custom_config: { @@ -110,7 +113,6 @@ describe('CustomWebAppBrand', () => { remove_webapp_brand: false, }, }, - mutateCurrentWorkspace: mutateMock, isCurrentWorkspaceManager: true, } as any) @@ -122,7 +124,7 @@ describe('CustomWebAppBrand', () => { url: '/workspaces/custom-config', body: { remove_webapp_brand: true }, })) - await waitFor(() => expect(mutateMock).toHaveBeenCalled()) + await waitFor(() => expect(mockInvalidateCurrentWorkspace).toHaveBeenCalled()) }) it('shows cancel/apply buttons after successful upload and cancels properly', async () => { diff --git a/web/app/components/custom/custom-web-app-brand/index.tsx b/web/app/components/custom/custom-web-app-brand/index.tsx index d9e80e80d1..3f5b6a25c6 100644 --- a/web/app/components/custom/custom-web-app-brand/index.tsx +++ b/web/app/components/custom/custom-web-app-brand/index.tsx @@ -24,6 +24,7 @@ import { useProviderContext } from '@/context/provider-context' import { updateCurrentWorkspace, } from '@/service/common' +import { useInvalidateCurrentWorkspace } from '@/service/use-common' import { cn } from '@/utils/classnames' const ALLOW_FILE_EXTENSIONS = ['svg', 'png'] @@ -34,9 +35,9 @@ const CustomWebAppBrand = () => { const { plan, enableBilling } = useProviderContext() const { currentWorkspace, - mutateCurrentWorkspace, isCurrentWorkspaceManager, } = useAppContext() + const invalidateCurrentWorkspace = useInvalidateCurrentWorkspace() const [fileId, setFileId] = useState('') const [imgKey, setImgKey] = useState(() => Date.now()) const [uploadProgress, setUploadProgress] = useState(0) @@ -83,7 +84,7 @@ const CustomWebAppBrand = () => { replace_webapp_logo: fileId, }, }) - mutateCurrentWorkspace() + invalidateCurrentWorkspace() setFileId('') setImgKey(Date.now()) } @@ -96,7 +97,7 @@ const CustomWebAppBrand = () => { replace_webapp_logo: '', }, }) - mutateCurrentWorkspace() + invalidateCurrentWorkspace() } const handleSwitch = async (checked: boolean) => { @@ -106,7 +107,7 @@ const CustomWebAppBrand = () => { remove_webapp_brand: checked, }, }) - mutateCurrentWorkspace() + invalidateCurrentWorkspace() } const handleCancel = () => { @@ -116,7 +117,7 @@ const CustomWebAppBrand = () => { return (
-
+
{t('webapp.removeBrand', { ns: 'custom' })} {
-
{t('webapp.changeLogo', { ns: 'custom' })}
-
{t('webapp.changeLogoTip', { ns: 'custom' })}
+
{t('webapp.changeLogo', { ns: 'custom' })}
+
{t('webapp.changeLogoTip', { ns: 'custom' })}
{(!uploadDisabled && webappLogo && !webappBrandRemoved) && ( @@ -204,7 +205,7 @@ const CustomWebAppBrand = () => {
{t('uploadedFail', { ns: 'custom' })}
)}
-
{t('overview.appInfo.preview', { ns: 'appOverview' })}
+
{t('overview.appInfo.preview', { ns: 'appOverview' })}
@@ -215,7 +216,7 @@ const CustomWebAppBrand = () => {
-
Chatflow App
+
Chatflow App
@@ -246,7 +247,7 @@ const CustomWebAppBrand = () => {
{!webappBrandRemoved && ( <> -
POWERED BY
+
POWERED BY
{ systemFeatures.branding.enabled && systemFeatures.branding.workspace_logo ? logo @@ -262,12 +263,12 @@ const CustomWebAppBrand = () => {
-
Hello! How can I assist you today?
+
Hello! How can I assist you today?
-
Talk to Dify
+
Talk to Dify
@@ -278,14 +279,14 @@ const CustomWebAppBrand = () => {
-
Workflow App
+
Workflow App
-
RUN ONCE
-
RUN BATCH
+
RUN ONCE
+
RUN BATCH
@@ -293,7 +294,7 @@ const CustomWebAppBrand = () => {
-
+