mirror of
https://github.com/langgenius/dify.git
synced 2026-09-08 11:04:27 +08:00
fix(web): clarify knowledge settings permission errors
Keep rejected Score Threshold drafts intact, show the localized permission message for 403 responses, and avoid duplicate error toasts.\n\nLinear: https://linear.app/dify/issue/WTA-2201/修复-score-threshold-保存时的权限错误提示
This commit is contained in:
parent
fbc7c85af1
commit
beb599c163
@ -949,6 +949,59 @@ describe('KnowledgeSettingsForm', () => {
|
||||
expect(thresholdSwitch).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('keeps a rejected score threshold draft and explains permission failures', async () => {
|
||||
const user = userEvent.setup()
|
||||
serviceMock.patchSettings.mockRejectedValueOnce(
|
||||
new Response(
|
||||
JSON.stringify({
|
||||
code: 'knowledge_fs_access_denied',
|
||||
message: 'You do not have permission to perform this KnowledgeFS operation.',
|
||||
status: 403,
|
||||
}),
|
||||
{ status: 403 },
|
||||
),
|
||||
)
|
||||
renderForm({
|
||||
settings: {
|
||||
...settings,
|
||||
retrieval: {
|
||||
...settings.retrieval,
|
||||
score_threshold: {
|
||||
...settings.retrieval.score_threshold,
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const thresholdInput = screen.getByRole('spinbutton', {
|
||||
name: 'appDebug.datasetConfig.score_threshold',
|
||||
})
|
||||
await user.clear(thresholdInput)
|
||||
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(),
|
||||
)
|
||||
})
|
||||
|
||||
it('requires a rerank model for a legacy knowledge base and saves it as enabled', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onDraftFinish = vi.fn()
|
||||
|
||||
@ -64,6 +64,10 @@ 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
|
||||
@ -286,7 +290,7 @@ export function KnowledgeSettingsForm({
|
||||
}),
|
||||
)
|
||||
const [nameTouched, setNameTouched] = useState(false)
|
||||
const [saveErrorSlice, setSaveErrorSlice] = useState<SaveErrorSlice>()
|
||||
const [saveError, setSaveError] = useState<SaveError>()
|
||||
const [isBasicRefreshing, setIsBasicRefreshing] = useState(false)
|
||||
const [pendingMigrationId, setPendingMigrationId] = useState<string>()
|
||||
const [pendingEmbeddingModel, setPendingEmbeddingModel] = useState<DefaultModel>()
|
||||
@ -408,7 +412,9 @@ export function KnowledgeSettingsForm({
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.externalAccess.put.mutationOptions(),
|
||||
)
|
||||
const settingsMutation = useMutation(
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.settings.patch.mutationOptions(),
|
||||
consoleQuery.knowledgeFs.spaces.byControlSpaceId.settings.patch.mutationOptions({
|
||||
context: { silent: true },
|
||||
}),
|
||||
)
|
||||
const migrationQuery = useQuery({
|
||||
...consoleQuery.knowledgeFs.spaces.byControlSpaceId.settings.migrations.byMigrationId.get.queryOptions(
|
||||
@ -463,7 +469,7 @@ export function KnowledgeSettingsForm({
|
||||
setVisibility(space.visibility)
|
||||
setSelectedMemberIds(initialSelectedMemberIds)
|
||||
setNameTouched(false)
|
||||
if (saveErrorSlice === 'basic') setSaveErrorSlice(undefined)
|
||||
if (saveError?.slice === 'basic') setSaveError(undefined)
|
||||
onDraftFinish?.()
|
||||
}
|
||||
|
||||
@ -537,7 +543,7 @@ export function KnowledgeSettingsForm({
|
||||
// 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.
|
||||
setSaveErrorSlice('settings')
|
||||
setSaveError({ kind: 'generic', slice: 'settings' })
|
||||
}
|
||||
}, [
|
||||
invalidateSettingsQueries,
|
||||
@ -550,7 +556,7 @@ export function KnowledgeSettingsForm({
|
||||
const performBasicSave = async () => {
|
||||
if (saveDisabled || !canEdit) return
|
||||
|
||||
setSaveErrorSlice(undefined)
|
||||
setSaveError(undefined)
|
||||
|
||||
try {
|
||||
const saveSlice = async (
|
||||
@ -605,7 +611,7 @@ export function KnowledgeSettingsForm({
|
||||
() => setIsBasicRefreshing(false),
|
||||
)
|
||||
} catch {
|
||||
setSaveErrorSlice('basic')
|
||||
setSaveError({ kind: 'generic', slice: 'basic' })
|
||||
}
|
||||
}
|
||||
|
||||
@ -621,7 +627,7 @@ export function KnowledgeSettingsForm({
|
||||
let nextDraft: ExternalAccessDraft | undefined = draft
|
||||
while (nextDraft) {
|
||||
queuedExternalAccessDraftRef.current = undefined
|
||||
setSaveErrorSlice(undefined)
|
||||
setSaveError(undefined)
|
||||
try {
|
||||
await externalAccessMutation.mutateAsync({
|
||||
body: {
|
||||
@ -639,7 +645,7 @@ export function KnowledgeSettingsForm({
|
||||
externalAccessSaveInFlightRef.current = false
|
||||
setApiEnabled(externalAccessBaselineRef.current.apiEnabled)
|
||||
setWorkflowEnabled(externalAccessBaselineRef.current.workflowEnabled)
|
||||
setSaveErrorSlice('externalAccess')
|
||||
setSaveError({ kind: 'generic', slice: 'externalAccess' })
|
||||
return
|
||||
}
|
||||
}
|
||||
@ -691,7 +697,7 @@ export function KnowledgeSettingsForm({
|
||||
}
|
||||
}
|
||||
|
||||
setSaveErrorSlice(undefined)
|
||||
setSaveError(undefined)
|
||||
try {
|
||||
const result = await settingsMutation.mutateAsync({
|
||||
body,
|
||||
@ -714,8 +720,11 @@ export function KnowledgeSettingsForm({
|
||||
}
|
||||
await invalidateSettingsQueries()
|
||||
return 'saved' as const
|
||||
} catch {
|
||||
setSaveErrorSlice('settings')
|
||||
} catch (error) {
|
||||
setSaveError({
|
||||
kind: error instanceof Response && error.status === 403 ? 'permission' : 'generic',
|
||||
slice: 'settings',
|
||||
})
|
||||
return 'failed' as const
|
||||
}
|
||||
}
|
||||
@ -799,11 +808,11 @@ export function KnowledgeSettingsForm({
|
||||
}
|
||||
|
||||
const retrySave = () => {
|
||||
if (saveErrorSlice === 'externalAccess') {
|
||||
if (saveError?.slice === 'externalAccess') {
|
||||
void performExternalAccessSave(pendingExternalAccessRef.current)
|
||||
return
|
||||
}
|
||||
if (saveErrorSlice === 'settings') {
|
||||
if (saveError?.slice === 'settings') {
|
||||
void performSettingsSave(pendingSettingsDraftRef.current ?? liveSettingsDraftRef.current)
|
||||
return
|
||||
}
|
||||
@ -976,14 +985,16 @@ export function KnowledgeSettingsForm({
|
||||
/>
|
||||
)}
|
||||
|
||||
{saveErrorSlice && (
|
||||
{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">
|
||||
{t(($) => $['newKnowledge.settings.saveFailed'])}
|
||||
{saveError.kind === 'permission'
|
||||
? t(($) => $['newKnowledge.permissionRestricted'])
|
||||
: t(($) => $['newKnowledge.settings.saveFailed'])}
|
||||
</span>
|
||||
<Button type="button" size="small" variant="ghost" onClick={retrySave}>
|
||||
{tCommon(($) => $['operation.retry'])}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user