mirror of
https://github.com/langgenius/dify.git
synced 2026-09-05 00:31:19 +08:00
Merge remote-tracking branch 'origin/deploy/konwledge' into deploy/konwledge
This commit is contained in:
commit
207640bcd1
@ -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,74 @@ 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 () => {
|
||||
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 () => {
|
||||
@ -732,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({
|
||||
@ -747,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({
|
||||
@ -784,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 () => {
|
||||
@ -837,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()
|
||||
@ -845,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()
|
||||
@ -869,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()
|
||||
})
|
||||
|
||||
@ -1109,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 () => {
|
||||
@ -1416,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', {
|
||||
@ -1450,6 +1486,9 @@ describe('KnowledgeSettingsForm', () => {
|
||||
},
|
||||
expect.anything(),
|
||||
)
|
||||
await waitFor(() =>
|
||||
expect(toastMock.success).toHaveBeenCalledWith('common.api.actionSuccess'),
|
||||
)
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
@ -1558,10 +1597,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', {
|
||||
@ -1817,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({
|
||||
@ -1831,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: {
|
||||
@ -1869,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', () => {
|
||||
|
||||
@ -69,7 +69,7 @@ export function KnowledgeSpaceCard({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<p className="relative z-1 line-clamp-2 min-h-8 w-full px-4 py-0.5 body-xs-regular text-text-tertiary">
|
||||
<p className="relative z-1 line-clamp-2 min-h-8 w-full px-4 py-0.5 body-xs-regular wrap-break-word text-text-tertiary">
|
||||
{summary?.description || t(($) => $['newKnowledge.noDescription'])}
|
||||
</p>
|
||||
</Link>
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -323,7 +323,7 @@ export function DocumentDetailPage({
|
||||
|
||||
const document = documentQuery.data
|
||||
return (
|
||||
<section className="flex min-h-0 flex-1 flex-col px-6 pt-3 pb-5 xl:px-8">
|
||||
<section className="flex min-h-0 flex-1 flex-col px-6 pt-3 pb-5">
|
||||
<KnowledgeModelReadinessBanner
|
||||
capability="index"
|
||||
className="mb-4"
|
||||
|
||||
@ -2568,8 +2568,8 @@ export function DocumentsPage({ knowledgeSpaceId }: { knowledgeSpaceId: string }
|
||||
<section
|
||||
ref={documentsSectionRef}
|
||||
className={cn(
|
||||
'relative flex min-h-full w-full flex-col gap-4 p-4 @min-[768px]/knowledge-content:p-6 @min-[1280px]/knowledge-content:p-8',
|
||||
bulkActionsVisible && 'pb-[calc(7rem+env(safe-area-inset-bottom,0px))]',
|
||||
'relative flex min-h-full w-full flex-col gap-4 px-6 pt-3',
|
||||
bulkActionsVisible ? 'pb-[calc(7rem+env(safe-area-inset-bottom,0px))]' : 'pb-6',
|
||||
)}
|
||||
onDragEnter={(event) => {
|
||||
const types = Array.from(event.dataTransfer.types ?? [])
|
||||
|
||||
@ -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<SaveError>()
|
||||
const [isBasicRefreshing, setIsBasicRefreshing] = useState(false)
|
||||
const [pendingMigrationId, setPendingMigrationId] = useState<string>()
|
||||
const [pendingEmbeddingModel, setPendingEmbeddingModel] = useState<DefaultModel>()
|
||||
@ -366,7 +360,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,15 +444,23 @@ 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 ||
|
||||
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<SettingsDraft>) => {
|
||||
if (!basicDirtyRef.current) settingsDraftOwnsFormLockRef.current = true
|
||||
@ -477,7 +478,6 @@ export function KnowledgeSettingsForm({
|
||||
setVisibility(space.visibility)
|
||||
setSelectedMemberIds(initialSelectedMemberIds)
|
||||
setNameTouched(false)
|
||||
if (saveError?.slice === 'basic') setSaveError(undefined)
|
||||
onDraftFinish?.()
|
||||
}
|
||||
|
||||
@ -524,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
|
||||
) {
|
||||
@ -550,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,
|
||||
@ -559,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 (
|
||||
@ -612,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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -635,7 +637,6 @@ export function KnowledgeSettingsForm({
|
||||
let nextDraft: ExternalAccessDraft | undefined = draft
|
||||
while (nextDraft) {
|
||||
queuedExternalAccessDraftRef.current = undefined
|
||||
setSaveError(undefined)
|
||||
try {
|
||||
await externalAccessMutation.mutateAsync({
|
||||
body: {
|
||||
@ -648,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
|
||||
}
|
||||
}
|
||||
@ -661,6 +660,7 @@ export function KnowledgeSettingsForm({
|
||||
externalAccessSaveInFlightRef.current = false
|
||||
setApiEnabled(externalAccessBaselineRef.current.apiEnabled)
|
||||
setWorkflowEnabled(externalAccessBaselineRef.current.workflowEnabled)
|
||||
showSaveSuccess()
|
||||
await invalidateSettingsQueries()
|
||||
}
|
||||
|
||||
@ -705,7 +705,6 @@ export function KnowledgeSettingsForm({
|
||||
}
|
||||
}
|
||||
|
||||
setSaveError(undefined)
|
||||
try {
|
||||
const result = await settingsMutation.mutateAsync({
|
||||
body,
|
||||
@ -729,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
|
||||
}
|
||||
}
|
||||
@ -754,10 +750,12 @@ export function KnowledgeSettingsForm({
|
||||
settingsSaveInFlightRef.current = true
|
||||
let nextDraft: SettingsDraft | undefined = draft
|
||||
let saveResult: Awaited<ReturnType<typeof saveSettingsDraft>> = '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
|
||||
}
|
||||
@ -781,6 +779,7 @@ export function KnowledgeSettingsForm({
|
||||
if (!latestDraftIsSaved) return
|
||||
|
||||
pendingSettingsDraftRef.current = undefined
|
||||
if (didSave) showSaveSuccess()
|
||||
if (settingsDraftOwnsFormLockRef.current && !basicDirtyRef.current) {
|
||||
settingsDraftOwnsFormLockRef.current = false
|
||||
onDraftFinish?.()
|
||||
@ -815,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
|
||||
@ -993,23 +980,6 @@ export function KnowledgeSettingsForm({
|
||||
/>
|
||||
)}
|
||||
|
||||
{saveError && (
|
||||
<div
|
||||
className="mb-3 flex items-center gap-2 rounded-lg border border-text-destructive/20 bg-background-default-subtle px-3 py-2"
|
||||
role="alert"
|
||||
>
|
||||
<span aria-hidden className="i-ri-error-warning-fill size-4 text-text-destructive" />
|
||||
<span className="min-w-0 flex-1 system-xs-regular text-text-destructive">
|
||||
{saveError.kind === 'permission'
|
||||
? t(($) => $['newKnowledge.permissionRestricted'])
|
||||
: t(($) => $['newKnowledge.settings.saveFailed'])}
|
||||
</span>
|
||||
<Button type="button" size="small" variant="ghost" onClick={retrySave}>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{serverConflict && (
|
||||
<div
|
||||
className="mb-3 flex items-center gap-2 rounded-lg border border-text-warning/20 bg-state-warning-hover px-3 py-2"
|
||||
@ -1022,16 +992,6 @@ export function KnowledgeSettingsForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{pendingMigrationId && (
|
||||
<div
|
||||
className="mb-3 flex items-center gap-2 rounded-lg border border-components-panel-border bg-background-section px-3 py-2 system-xs-regular text-text-tertiary"
|
||||
role="status"
|
||||
>
|
||||
<span aria-hidden className="i-ri-loader-4-line size-4 shrink-0 animate-spin" />
|
||||
{tCommon(($) => $['operation.saving'])}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Form className="flex flex-col gap-4 overflow-hidden pt-2 pb-7" onSubmit={requestSave}>
|
||||
<h2 className="flex h-8 items-center system-sm-semibold text-text-secondary">
|
||||
{t(($) => $['newKnowledge.settings.basicInfo'])}
|
||||
@ -1151,9 +1111,7 @@ export function KnowledgeSettingsForm({
|
||||
{tCommon(($) => $['operation.cancel'])}
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" disabled={saveDisabled} loading={isBasicSaving}>
|
||||
{isBasicSaving
|
||||
? tCommon(($) => $['operation.saving'])
|
||||
: t(($) => $['newKnowledge.settings.saveChanges'])}
|
||||
{t(($) => $['newKnowledge.settings.saveChanges'])}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@ -1360,7 +1318,6 @@ export function KnowledgeSettingsForm({
|
||||
<div className="flex items-center gap-3">
|
||||
<NumberField
|
||||
id="knowledge-top-k"
|
||||
name="knowledge-top-k"
|
||||
min={TOP_K_MIN}
|
||||
max={TOP_K_MAX}
|
||||
step={1}
|
||||
@ -1425,7 +1382,6 @@ export function KnowledgeSettingsForm({
|
||||
<div className="flex items-center gap-3">
|
||||
<NumberField
|
||||
id="knowledge-score-threshold"
|
||||
name="knowledge-score-threshold"
|
||||
min={SCORE_THRESHOLD_MIN}
|
||||
max={SCORE_THRESHOLD_MAX}
|
||||
step={0.01}
|
||||
|
||||
@ -299,7 +299,7 @@ export function SourcesPage({ knowledgeSpaceId }: { knowledgeSpaceId: string })
|
||||
])
|
||||
|
||||
return (
|
||||
<div className="flex min-h-full min-w-0 flex-1 flex-col p-4 @min-[768px]/knowledge-content:p-6 @min-[1280px]/knowledge-content:p-8">
|
||||
<div className="flex min-h-full min-w-0 flex-1 flex-col px-6 pt-3 pb-6 sm:pb-8">
|
||||
<header className="flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<h2 className="title-xl-semi-bold leading-6 text-text-primary">
|
||||
|
||||
Loading…
Reference in New Issue
Block a user