From d002f602727b3d1cbeca923363c85fd59c38b428 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:25:30 +0800 Subject: [PATCH 1/6] fix(knowledge_fs): prevent settings page overflow (WTA-2224) --- web/features/new-rag/knowledge-settings-form.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/web/features/new-rag/knowledge-settings-form.tsx b/web/features/new-rag/knowledge-settings-form.tsx index 9631d0881fb..4a3fd1f8858 100644 --- a/web/features/new-rag/knowledge-settings-form.tsx +++ b/web/features/new-rag/knowledge-settings-form.tsx @@ -1360,7 +1360,6 @@ export function KnowledgeSettingsForm({
Date: Wed, 26 Aug 2026 11:38:50 +0800 Subject: [PATCH 2/6] fix(knowledge_fs): allow updating external access before profile activation --- .../knowledge-settings-form.spec.tsx | 71 ++++++++++++++++++- .../new-rag/knowledge-settings-form.tsx | 3 +- 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx b/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx index 2c2a4bb4cfc..71b9e0f2ece 100644 --- a/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx +++ b/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx @@ -627,6 +627,73 @@ describe('KnowledgeSettingsForm', () => { expect(toastMock.success).not.toHaveBeenCalled() }) + it('keeps access channels submittable before the first model profile activates', async () => { + const user = userEvent.setup() + renderForm({ + settings: { + ...settings, + active_profile_available: false, + active_profile_revisions: {}, + capabilities: { + deep: false, + index: false, + ingest: true, + query: false, + research: false, + source_sync: true, + }, + }, + }) + + const apiAccessSwitch = screen.getByRole('switch', { + name: 'dataset.newKnowledge.apiAgentAccess', + }) + const workflowAccessSwitch = screen.getByRole('switch', { + name: 'dataset.newKnowledge.workflowAccess', + }) + expect(apiAccessSwitch).not.toHaveAttribute('aria-disabled', 'true') + expect(workflowAccessSwitch).not.toHaveAttribute('aria-disabled', 'true') + expect(apiAccessSwitch).toHaveAccessibleDescription( + 'dataset.newKnowledge.settings.apiAccessDescription', + ) + + await user.click(apiAccessSwitch) + + await waitFor(() => { + expect(serviceMock.patchExternalAccess).toHaveBeenCalledWith( + { + body: { + agent_enabled: false, + mcp_enabled: true, + service_api_enabled: false, + workflow_enabled: true, + }, + params: { control_space_id: 'space-1' }, + }, + expect.anything(), + ) + }) + await waitFor(() => expect(apiAccessSwitch).toHaveAttribute('aria-checked', 'false')) + expect(apiAccessSwitch).not.toHaveAttribute('aria-disabled', 'true') + expect(workflowAccessSwitch).not.toHaveAttribute('aria-disabled', 'true') + + await user.click(apiAccessSwitch) + + await waitFor(() => expect(serviceMock.patchExternalAccess).toHaveBeenCalledTimes(2)) + expect(serviceMock.patchExternalAccess).toHaveBeenLastCalledWith( + { + body: { + agent_enabled: true, + mcp_enabled: true, + service_api_enabled: true, + workflow_enabled: true, + }, + params: { control_space_id: 'space-1' }, + }, + expect.anything(), + ) + }) + it('keeps unrelated form controls interactive while external access is saving', async () => { const user = userEvent.setup() let finishExternalAccessSave!: (value: typeof externalAccess) => void @@ -1558,10 +1625,10 @@ describe('KnowledgeSettingsForm', () => { ) expect( screen.getByRole('switch', { name: 'dataset.newKnowledge.apiAgentAccess' }), - ).toHaveAttribute('aria-disabled', 'true') + ).not.toHaveAttribute('aria-disabled', 'true') expect( screen.getByRole('switch', { name: 'dataset.newKnowledge.workflowAccess' }), - ).toHaveAttribute('aria-disabled', 'true') + ).not.toHaveAttribute('aria-disabled', 'true') await user.click( screen.getByRole('button', { diff --git a/web/features/new-rag/knowledge-settings-form.tsx b/web/features/new-rag/knowledge-settings-form.tsx index 4a3fd1f8858..dd647c36fc1 100644 --- a/web/features/new-rag/knowledge-settings-form.tsx +++ b/web/features/new-rag/knowledge-settings-form.tsx @@ -366,7 +366,6 @@ export function KnowledgeSettingsForm({ const canManageAccess = space.permission_keys.includes('knowledge_space_access_config') const canDelete = space.permission_keys.includes('knowledge_space_delete') const initialModelSetup = !settings.active_profile_available - const modelSetupReady = settings.active_profile_available const readinessFieldLabel = (field: KnowledgeFsSettingsResponse['issues'][number]['field']) => { if (field === 'embedding') return tSettings(($) => $['form.embeddingModel']) if (field === 'reasoning') return tCommon(($) => $['modelProvider.systemReasoningModel.key']) @@ -451,7 +450,7 @@ export function KnowledgeSettingsForm({ settingsMutation.isPending || Boolean(pendingMigrationId) const basicFieldsDisabled = !canEdit || isBasicSaving - const externalAccessDisabled = !canEdit || !canManageAccess || !modelSetupReady + const externalAccessDisabled = !canEdit || !canManageAccess const retrievalFieldsDisabled = !canEdit || (!initialModelSetup && embeddingDirty) const saveDisabled = !basicDirty || From faf7c29900378e69c781ff4c694b833721193ada Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 26 Aug 2026 11:53:54 +0800 Subject: [PATCH 3/6] fix(knowledge_fs): wrap long card descriptions (WTA-2226) --- web/features/new-rag/components/knowledge-space-card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/features/new-rag/components/knowledge-space-card.tsx b/web/features/new-rag/components/knowledge-space-card.tsx index 3d259dd2f84..b76abb97398 100644 --- a/web/features/new-rag/components/knowledge-space-card.tsx +++ b/web/features/new-rag/components/knowledge-space-card.tsx @@ -69,7 +69,7 @@ export function KnowledgeSpaceCard({
-

+

{summary?.description || t(($) => $['newKnowledge.noDescription'])}

From 677eeaf5a224408c6e6d6e9a0dd052c1c0f75e6b Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:03:03 +0800 Subject: [PATCH 4/6] fix(knowledge_fs): use standard toasts for settings saves (WTA-2227) --- .../knowledge-settings-form.spec.tsx | 138 +++++++----------- .../new-rag/knowledge-settings-form.tsx | 109 +++++--------- 2 files changed, 89 insertions(+), 158 deletions(-) diff --git a/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx b/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx index 71b9e0f2ece..732f3d26214 100644 --- a/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx +++ b/web/features/new-rag/__tests__/knowledge-settings-form.spec.tsx @@ -362,11 +362,10 @@ describe('KnowledgeSettingsForm', () => { const nameInput = screen.getByRole('textbox', { name: 'datasetSettings.form.name' }) await user.clear(nameInput) await user.type(nameInput, 'Updated camera specs') - await user.click( - screen.getByRole('button', { - name: 'dataset.newKnowledge.settings.saveChanges', - }), - ) + const saveButton = screen.getByRole('button', { + name: 'dataset.newKnowledge.settings.saveChanges', + }) + await user.click(saveButton) await waitFor(() => { expect(serviceMock.patchSpace).toHaveBeenCalledWith( @@ -380,7 +379,7 @@ describe('KnowledgeSettingsForm', () => { ) }) expect(serviceMock.patchSettings).not.toHaveBeenCalled() - expect(toastMock.success).not.toHaveBeenCalled() + expect(toastMock.success).toHaveBeenCalledWith('common.api.actionSuccess') }) it('finishes the basic info draft before refreshing saved server data', async () => { @@ -433,6 +432,12 @@ describe('KnowledgeSettingsForm', () => { ) await waitFor(() => expect(serviceMock.patchSpace).toHaveBeenCalledOnce()) + const saveButton = screen.getByRole('button', { + name: 'dataset.newKnowledge.settings.saveChanges', + }) + expect(saveButton).toHaveAttribute('aria-disabled', 'true') + expect(saveButton).toHaveTextContent('dataset.newKnowledge.settings.saveChanges') + expect(screen.queryByText('common.operation.saving')).not.toBeInTheDocument() const apiAccessSwitch = screen.getByRole('switch', { name: 'dataset.newKnowledge.apiAgentAccess', }) @@ -624,7 +629,7 @@ describe('KnowledgeSettingsForm', () => { name: 'dataset.newKnowledge.settings.saveChanges', }), ).toBeDisabled() - expect(toastMock.success).not.toHaveBeenCalled() + expect(toastMock.success).toHaveBeenCalledWith('common.api.actionSuccess') }) it('keeps access channels submittable before the first model profile activates', async () => { @@ -799,7 +804,7 @@ describe('KnowledgeSettingsForm', () => { await waitFor(() => expect(workflowAccessSwitch).toHaveAttribute('aria-checked', 'true')) }) - it('restores the Workflow access switch after failure and retries the intended value', async () => { + it('keeps the Workflow access edit and shows an error toast after failure', async () => { const user = userEvent.setup() serviceMock.patchExternalAccess.mockRejectedValueOnce(new Error('network error')) renderForm({ @@ -814,27 +819,14 @@ describe('KnowledgeSettingsForm', () => { }) await user.click(workflowAccessSwitch) - expect(await screen.findByText('dataset.newKnowledge.settings.saveFailed')).toBeInTheDocument() - expect(workflowAccessSwitch).toHaveAttribute('aria-checked', 'false') - await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) - - await waitFor(() => expect(serviceMock.patchExternalAccess).toHaveBeenCalledTimes(2)) - expect(serviceMock.patchExternalAccess).toHaveBeenLastCalledWith( - { - body: { - agent_enabled: true, - mcp_enabled: true, - service_api_enabled: true, - workflow_enabled: true, - }, - params: { control_space_id: 'space-1' }, - }, - expect.anything(), + await waitFor(() => + expect(toastMock.error).toHaveBeenCalledWith('dataset.newKnowledge.settings.saveFailed'), ) - await waitFor(() => expect(workflowAccessSwitch).toHaveAttribute('aria-checked', 'true')) + expect(workflowAccessSwitch).toHaveAttribute('aria-checked', 'true') + expect(screen.queryByText('dataset.newKnowledge.settings.saveFailed')).not.toBeInTheDocument() }) - it('restores the API access switch after failure and retries the intended value', async () => { + it('keeps the API access edit and shows an error toast after failure', async () => { const user = userEvent.setup() serviceMock.patchExternalAccess.mockRejectedValueOnce(new Error('network error')) renderForm({ @@ -851,24 +843,11 @@ describe('KnowledgeSettingsForm', () => { expect(apiAccessSwitch).toHaveAttribute('aria-checked', 'false') await user.click(apiAccessSwitch) - expect(await screen.findByText('dataset.newKnowledge.settings.saveFailed')).toBeInTheDocument() - expect(apiAccessSwitch).toHaveAttribute('aria-checked', 'false') - await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) - - await waitFor(() => expect(serviceMock.patchExternalAccess).toHaveBeenCalledTimes(2)) - expect(serviceMock.patchExternalAccess).toHaveBeenLastCalledWith( - { - body: { - agent_enabled: true, - mcp_enabled: true, - service_api_enabled: true, - workflow_enabled: true, - }, - params: { control_space_id: 'space-1' }, - }, - expect.anything(), + await waitFor(() => + expect(toastMock.error).toHaveBeenCalledWith('dataset.newKnowledge.settings.saveFailed'), ) - await waitFor(() => expect(apiAccessSwitch).toHaveAttribute('aria-checked', 'true')) + expect(apiAccessSwitch).toHaveAttribute('aria-checked', 'true') + expect(screen.queryByText('dataset.newKnowledge.settings.saveFailed')).not.toBeInTheDocument() }) it('requires the exact knowledge name before deletion', async () => { @@ -904,7 +883,7 @@ describe('KnowledgeSettingsForm', () => { expect(routerMock.replace).toHaveBeenCalledWith('/datasets?view=new') }) - it('keeps edits and offers retry after saving fails', async () => { + it('keeps edits and shows a standard error toast after saving fails', async () => { const user = userEvent.setup() serviceMock.patchSpace.mockRejectedValueOnce(new Error('network error')) renderForm() @@ -912,20 +891,24 @@ describe('KnowledgeSettingsForm', () => { const nameInput = screen.getByRole('textbox', { name: 'datasetSettings.form.name' }) await user.clear(nameInput) await user.type(nameInput, 'Camera specs draft') - await user.click( - screen.getByRole('button', { - name: 'dataset.newKnowledge.settings.saveChanges', - }), + const saveButton = screen.getByRole('button', { + name: 'dataset.newKnowledge.settings.saveChanges', + }) + await user.click(saveButton) + + await waitFor(() => + expect(toastMock.error).toHaveBeenCalledWith('dataset.newKnowledge.settings.saveFailed'), ) - - expect(await screen.findByText('dataset.newKnowledge.settings.saveFailed')).toBeInTheDocument() expect(nameInput).toHaveValue('Camera specs draft') + expect(screen.queryByText('dataset.newKnowledge.settings.saveFailed')).not.toBeInTheDocument() + expect(saveButton).toBeEnabled() - await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) + await user.click(saveButton) await waitFor(() => expect(serviceMock.patchSpace).toHaveBeenCalledTimes(2)) + expect(toastMock.success).toHaveBeenCalledWith('common.api.actionSuccess') }) - it('retries an immediate retrieval settings update without submitting basic info', async () => { + it('shows an error toast for a failed retrieval settings update without submitting basic info', async () => { const user = userEvent.setup() serviceMock.patchSettings.mockRejectedValueOnce(new Error('settings unavailable')) renderForm() @@ -936,10 +919,10 @@ describe('KnowledgeSettingsForm', () => { }), ) - expect(await screen.findByText('dataset.newKnowledge.settings.saveFailed')).toBeInTheDocument() - await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) - - await waitFor(() => expect(serviceMock.patchSettings).toHaveBeenCalledTimes(2)) + await waitFor(() => + expect(toastMock.error).toHaveBeenCalledWith('dataset.newKnowledge.settings.saveFailed'), + ) + expect(screen.queryByText('dataset.newKnowledge.settings.saveFailed')).not.toBeInTheDocument() expect(serviceMock.patchSpace).not.toHaveBeenCalled() }) @@ -1176,25 +1159,11 @@ describe('KnowledgeSettingsForm', () => { await user.type(thresholdInput, '0.72') await user.tab() - expect(await screen.findByText('dataset.newKnowledge.permissionRestricted')).toBeInTheDocument() - expect(screen.queryByText('dataset.newKnowledge.settings.saveFailed')).not.toBeInTheDocument() - expect(thresholdInput).toHaveValue('0.72') - - await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) - - await waitFor(() => expect(serviceMock.patchSettings).toHaveBeenCalledTimes(2)) - expect(serviceMock.patchSettings).toHaveBeenLastCalledWith( - { - body: { - expectedRevision: 5, - retrieval: expect.objectContaining({ - scoreThreshold: expect.objectContaining({ value: 0.72 }), - }), - }, - params: { control_space_id: 'space-1' }, - }, - expect.anything(), + await waitFor(() => + expect(toastMock.error).toHaveBeenCalledWith('dataset.newKnowledge.permissionRestricted'), ) + expect(screen.queryByText('dataset.newKnowledge.permissionRestricted')).not.toBeInTheDocument() + expect(thresholdInput).toHaveValue('0.72') }) it('requires a rerank model for a legacy knowledge base and saves it as enabled', async () => { @@ -1483,7 +1452,7 @@ describe('KnowledgeSettingsForm', () => { name: 'dataset.newKnowledge.settings.systemReasoningModelLabel', }), ) - expect(await screen.findByRole('status')).toHaveTextContent('common.operation.saving') + expect(screen.queryByText('common.operation.saving')).not.toBeInTheDocument() fireEvent.change( screen.getByRole('textbox', { @@ -1517,6 +1486,9 @@ describe('KnowledgeSettingsForm', () => { }, expect.anything(), ) + await waitFor(() => + expect(toastMock.success).toHaveBeenCalledWith('common.api.actionSuccess'), + ) } finally { vi.useRealTimers() } @@ -1884,7 +1856,7 @@ describe('KnowledgeSettingsForm', () => { }), ) - expect(await screen.findByRole('status')).toHaveTextContent('common.operation.saving') + expect(screen.queryByText('common.operation.saving')).not.toBeInTheDocument() expect(toastMock.success).not.toHaveBeenCalled() resolveMigration({ @@ -1898,11 +1870,11 @@ describe('KnowledgeSettingsForm', () => { updated_at: '2026-07-28T00:01:00Z', }) - await waitFor(() => expect(screen.queryByRole('status')).not.toBeInTheDocument()) - expect(toastMock.success).not.toHaveBeenCalled() + await waitFor(() => expect(toastMock.success).toHaveBeenCalledWith('common.api.actionSuccess')) + expect(screen.queryByText('common.operation.saving')).not.toBeInTheDocument() }) - it('offers retry when a durable profile migration fails', async () => { + it('shows an error toast when a durable profile migration fails', async () => { const user = userEvent.setup() serviceMock.patchSettings.mockResolvedValueOnce({ migration: { @@ -1936,11 +1908,11 @@ describe('KnowledgeSettingsForm', () => { }), ) - expect(await screen.findByText('dataset.newKnowledge.settings.saveFailed')).toBeInTheDocument() + await waitFor(() => + expect(toastMock.error).toHaveBeenCalledWith('dataset.newKnowledge.settings.saveFailed'), + ) + expect(screen.queryByText('dataset.newKnowledge.settings.saveFailed')).not.toBeInTheDocument() expect(toastMock.success).not.toHaveBeenCalled() - await user.click(screen.getByRole('button', { name: 'common.operation.retry' })) - - await waitFor(() => expect(serviceMock.patchSettings).toHaveBeenCalledTimes(2)) }) it('fully locks the page for a view-only user', () => { diff --git a/web/features/new-rag/knowledge-settings-form.tsx b/web/features/new-rag/knowledge-settings-form.tsx index dd647c36fc1..e31650de97c 100644 --- a/web/features/new-rag/knowledge-settings-form.tsx +++ b/web/features/new-rag/knowledge-settings-form.tsx @@ -71,11 +71,6 @@ const RERANK_MODEL_LABEL_ID = 'knowledge-rerank-model-label' const RERANK_MODEL_ERROR_ID = 'knowledge-rerank-model-error' type BasicSaveSlice = 'members' | 'space' -type SaveErrorSlice = 'basic' | 'externalAccess' | 'settings' -type SaveError = { - kind: 'generic' | 'permission' - slice: SaveErrorSlice -} type ExternalAccessDraft = { apiEnabled: boolean @@ -298,7 +293,6 @@ export function KnowledgeSettingsForm({ }), ) const [nameTouched, setNameTouched] = useState(false) - const [saveError, setSaveError] = useState() const [isBasicRefreshing, setIsBasicRefreshing] = useState(false) const [pendingMigrationId, setPendingMigrationId] = useState() const [pendingEmbeddingModel, setPendingEmbeddingModel] = useState() @@ -453,12 +447,20 @@ export function KnowledgeSettingsForm({ const externalAccessDisabled = !canEdit || !canManageAccess const retrievalFieldsDisabled = !canEdit || (!initialModelSetup && embeddingDirty) const saveDisabled = - !basicDirty || - nameInvalid || - descriptionInvalid || - membersInvalid || - isBasicSaving || - serverConflict + !basicDirty || nameInvalid || descriptionInvalid || membersInvalid || serverConflict + const showSaveSuccess = useCallback( + () => toast.success(tCommon(($) => $['api.actionSuccess'])), + [tCommon], + ) + const showSaveError = useCallback( + (error?: unknown) => + toast.error( + error instanceof Response && error.status === 403 + ? t(($) => $['newKnowledge.permissionRestricted']) + : t(($) => $['newKnowledge.settings.saveFailed']), + ), + [t], + ) const startDraft = () => onDraftStart?.() const beginSettingsDraft = (patch: Partial) => { if (!basicDirtyRef.current) settingsDraftOwnsFormLockRef.current = true @@ -476,7 +478,6 @@ export function KnowledgeSettingsForm({ setVisibility(space.visibility) setSelectedMemberIds(initialSelectedMemberIds) setNameTouched(false) - if (saveError?.slice === 'basic') setSaveError(undefined) onDraftFinish?.() } @@ -523,12 +524,14 @@ export function KnowledgeSettingsForm({ setRetrievalBaseline(savedRetrievalFingerprint) } void invalidateSettingsQueries().then(() => { + const hasQueuedSettingsDraft = Boolean(queuedSettingsDraftRef.current) migratingSettingsDraftRef.current = undefined - if (!queuedSettingsDraftRef.current) pendingSettingsDraftRef.current = undefined - resumeSettingsQueueAfterMigrationRef.current = Boolean(queuedSettingsDraftRef.current) + if (!hasQueuedSettingsDraft) pendingSettingsDraftRef.current = undefined + resumeSettingsQueueAfterMigrationRef.current = hasQueuedSettingsDraft setPendingMigrationId(undefined) + if (!hasQueuedSettingsDraft) showSaveSuccess() if ( - !queuedSettingsDraftRef.current && + !hasQueuedSettingsDraft && settingsDraftOwnsFormLockRef.current && !basicDirtyRef.current ) { @@ -549,8 +552,7 @@ export function KnowledgeSettingsForm({ resumeSettingsQueueAfterMigrationRef.current = false // oxlint-disable-next-line eslint-react/set-state-in-effect -- A terminal remote migration retires the local polling guard. setPendingMigrationId(undefined) - // oxlint-disable-next-line eslint-react/set-state-in-effect -- The authoritative failed migration transitions the save UI to its retry state. - setSaveError({ kind: 'generic', slice: 'settings' }) + showSaveError() } }, [ invalidateSettingsQueries, @@ -558,12 +560,12 @@ export function KnowledgeSettingsForm({ migrationQuery.isError, onDraftFinish, pendingMigrationId, + showSaveError, + showSaveSuccess, ]) const performBasicSave = async () => { - if (saveDisabled || !canEdit) return - - setSaveError(undefined) + if (saveDisabled || isBasicSaving || !canEdit) return try { const saveSlice = async ( @@ -611,14 +613,15 @@ export function KnowledgeSettingsForm({ ) } completedBasicSaveFingerprintsRef.current = {} + showSaveSuccess() setIsBasicRefreshing(true) onDraftFinish?.() void invalidateSettingsQueries().then( () => setIsBasicRefreshing(false), () => setIsBasicRefreshing(false), ) - } catch { - setSaveError({ kind: 'generic', slice: 'basic' }) + } catch (error) { + showSaveError(error) } } @@ -634,7 +637,6 @@ export function KnowledgeSettingsForm({ let nextDraft: ExternalAccessDraft | undefined = draft while (nextDraft) { queuedExternalAccessDraftRef.current = undefined - setSaveError(undefined) try { await externalAccessMutation.mutateAsync({ body: { @@ -647,12 +649,10 @@ export function KnowledgeSettingsForm({ }) externalAccessBaselineRef.current = nextDraft nextDraft = queuedExternalAccessDraftRef.current - } catch { + } catch (error) { queuedExternalAccessDraftRef.current = undefined externalAccessSaveInFlightRef.current = false - setApiEnabled(externalAccessBaselineRef.current.apiEnabled) - setWorkflowEnabled(externalAccessBaselineRef.current.workflowEnabled) - setSaveError({ kind: 'generic', slice: 'externalAccess' }) + showSaveError(error) return } } @@ -660,6 +660,7 @@ export function KnowledgeSettingsForm({ externalAccessSaveInFlightRef.current = false setApiEnabled(externalAccessBaselineRef.current.apiEnabled) setWorkflowEnabled(externalAccessBaselineRef.current.workflowEnabled) + showSaveSuccess() await invalidateSettingsQueries() } @@ -704,7 +705,6 @@ export function KnowledgeSettingsForm({ } } - setSaveError(undefined) try { const result = await settingsMutation.mutateAsync({ body, @@ -728,10 +728,7 @@ export function KnowledgeSettingsForm({ await invalidateSettingsQueries() return 'saved' as const } catch (error) { - setSaveError({ - kind: error instanceof Response && error.status === 403 ? 'permission' : 'generic', - slice: 'settings', - }) + showSaveError(error) return 'failed' as const } } @@ -753,10 +750,12 @@ export function KnowledgeSettingsForm({ settingsSaveInFlightRef.current = true let nextDraft: SettingsDraft | undefined = draft let saveResult: Awaited> = 'skipped' + let didSave = false try { while (nextDraft) { queuedSettingsDraftRef.current = undefined saveResult = await saveSettingsDraft(nextDraft) + if (saveResult === 'saved') didSave = true if (saveResult === 'failed' || saveResult === 'migration') break nextDraft = queuedSettingsDraftRef.current } @@ -780,6 +779,7 @@ export function KnowledgeSettingsForm({ if (!latestDraftIsSaved) return pendingSettingsDraftRef.current = undefined + if (didSave) showSaveSuccess() if (settingsDraftOwnsFormLockRef.current && !basicDirtyRef.current) { settingsDraftOwnsFormLockRef.current = false onDraftFinish?.() @@ -814,18 +814,6 @@ export function KnowledgeSettingsForm({ void performBasicSave() } - const retrySave = () => { - if (saveError?.slice === 'externalAccess') { - void performExternalAccessSave(pendingExternalAccessRef.current) - return - } - if (saveError?.slice === 'settings') { - void performSettingsSave(pendingSettingsDraftRef.current ?? liveSettingsDraftRef.current) - return - } - requestSave() - } - const armHistoryGuard = useCallback(() => { globalThis.history.pushState(globalThis.history.state, '', globalThis.location.href) historyGuardArmedRef.current = true @@ -992,23 +980,6 @@ export function KnowledgeSettingsForm({ /> )} - {saveError && ( -
- - - {saveError.kind === 'permission' - ? t(($) => $['newKnowledge.permissionRestricted']) - : t(($) => $['newKnowledge.settings.saveFailed'])} - - -
- )} - {serverConflict && (
)} - {pendingMigrationId && ( -
- - {tCommon(($) => $['operation.saving'])} -
- )} -

{t(($) => $['newKnowledge.settings.basicInfo'])} @@ -1150,9 +1111,7 @@ export function KnowledgeSettingsForm({ {tCommon(($) => $['operation.cancel'])}

)} From 30435e8123b8a117d3d7e1b02744987ac1b3a9e7 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:15:16 +0800 Subject: [PATCH 5/6] fix(knowledge_fs): align page headers with sidebar (WTA-2229) --- web/features/new-rag/document-detail-page.tsx | 2 +- web/features/new-rag/documents-page.tsx | 4 ++-- web/features/new-rag/sources-page.tsx | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/features/new-rag/document-detail-page.tsx b/web/features/new-rag/document-detail-page.tsx index 62fd56ae915..aa7a31cc179 100644 --- a/web/features/new-rag/document-detail-page.tsx +++ b/web/features/new-rag/document-detail-page.tsx @@ -323,7 +323,7 @@ export function DocumentDetailPage({ const document = documentQuery.data return ( -
+
{ const types = Array.from(event.dataTransfer.types ?? []) diff --git a/web/features/new-rag/sources-page.tsx b/web/features/new-rag/sources-page.tsx index b4c641f6356..10a9b6b771f 100644 --- a/web/features/new-rag/sources-page.tsx +++ b/web/features/new-rag/sources-page.tsx @@ -299,7 +299,7 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }) ]) return ( -
+

From 3cc31cf9b5f8c636ae43003a88824b34ad198bd2 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Wed, 26 Aug 2026 12:23:48 +0800 Subject: [PATCH 6/6] fix(knowledge_fs): indent document contents by nesting level (WTA-2230) --- web/features/new-rag/document-chunk-tree.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/features/new-rag/document-chunk-tree.tsx b/web/features/new-rag/document-chunk-tree.tsx index 111e16c868c..8282de55983 100644 --- a/web/features/new-rag/document-chunk-tree.tsx +++ b/web/features/new-rag/document-chunk-tree.tsx @@ -11,6 +11,8 @@ import { chunkTreeLabel, visibleDocumentChunkNodes } from './document-detail-mod const VIRTUALIZATION_THRESHOLD = 80 const TREE_ROW_SIZE = 30 +const TREE_ROW_INLINE_PADDING = 8 +const TREE_DEPTH_INDENT = 16 function AutomaticChunkPageLoader({ fetchNextPage, @@ -204,7 +206,10 @@ export function DocumentChunkTreePanel({ 'bg-state-base-hover ring-1 ring-state-accent-solid ring-inset', )} role="treeitem" - style={style} + style={{ + ...style, + paddingInlineStart: TREE_ROW_INLINE_PADDING + depth * TREE_DEPTH_INDENT, + }} tabIndex={-1} onClick={() => { setFocusedNodeId(node.id)