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'])}
-
- )}
-
)}
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)