refactor(approval-ux): eng-review fixes — extract helpers, add tests, register i18n

This commit is contained in:
Александр Зимин
2026-05-14 13:52:12 +00:00
parent 31ad140a02
commit 4c29f4e7cb
8 changed files with 365 additions and 109 deletions
@@ -52,6 +52,42 @@ const SCOPE_OPTIONS = (t: (k: string) => string): SelectOption[] => [
type EditorTab = 'metadata' | 'schema' | 'preview' | 'events'
/**
* Decide whether the editor's Save button should be blocked because the
* change has to go through the schema-draft workflow instead of inline PUT.
* Mirrors the three backend gate clauses in DictionaryDefinitionService
* exactly so the UX matches the server contract.
*
* Returns false (don't block) for create-mode, for non-approval-required
* dicts, and for pure metadata edits (display name / description / locales
* etc.) that leave schema + approval config alone.
*
* Exported for unit testability — the three-clause logic is the load-bearing
* piece, easier to verify in isolation than mounting the full editor with
* its query and mutation deps.
*/
export function computeRequiresDraftFlow({
isEdit,
initialApprovalRequired,
initialMinRole,
currentApprovalRequired,
currentMinRole,
schemaChangeCount,
}: {
isEdit: boolean
initialApprovalRequired: boolean
initialMinRole: string | null
currentApprovalRequired: boolean
currentMinRole: string | null
schemaChangeCount: number
}): boolean {
if (!isEdit || !initialApprovalRequired) return false
const schemaChanged = schemaChangeCount > 0
const approvalToggledOff = !currentApprovalRequired
const minRoleChanged = initialMinRole !== currentMinRole
return schemaChanged || approvalToggledOff || minRoleChanged
}
export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props) => {
const { t } = useTranslation()
const createMut = useCreateDictionary()
@@ -150,17 +186,17 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
// 422 approval_required, but it's much better UX to disable Save up front
// and tell the user to open a draft instead. Mirror the three backend
// gate clauses exactly so the UX matches the server contract.
const wasApprovalRequired =
Boolean((initial as { approvalRequired?: boolean } | null)?.approvalRequired)
const initialMinRole =
((initial as { approvalMinRole?: string } | null)?.approvalMinRole ?? '') || null
const currentMinRole = approvalMinRole.trim() === '' ? null : approvalMinRole.trim()
const schemaChangedFromInitial = isEdit && changes.length > 0
const approvalToggledOff = isEdit && wasApprovalRequired && !approvalRequired
const minRoleChanged = isEdit && wasApprovalRequired && initialMinRole !== currentMinRole
const requiresDraftFlow =
isEdit && wasApprovalRequired
&& (schemaChangedFromInitial || approvalToggledOff || minRoleChanged)
const requiresDraftFlow = computeRequiresDraftFlow({
isEdit,
initialApprovalRequired: Boolean(
(initial as { approvalRequired?: boolean } | null)?.approvalRequired,
),
initialMinRole:
((initial as { approvalMinRole?: string } | null)?.approvalMinRole ?? '') || null,
currentApprovalRequired: approvalRequired,
currentMinRole: approvalMinRole.trim() === '' ? null : approvalMinRole.trim(),
schemaChangeCount: changes.length,
})
const activeMutation = isEdit ? updateMut : createMut
const serverError = serverErrorMessage(activeMutation.error)
@@ -490,9 +526,13 @@ export const DictionaryEditorDialog = ({ open, mode, onClose, onSuccess }: Props
detail={initial}
initialProposedSchema={schemaJson}
onCreated={() => {
// Cascade close: dismiss the draft modal AND the editor. Once
// the draft is submitted, the in-memory edit in the editor is
// stale by design — the draft is now the source of truth until
// a reviewer decides. Leaving the editor open would let the
// maker silently mutate further state that can't go anywhere
// (Save still disabled until the draft resolves).
setDraftHandoffOpen(false)
// Close the editor too — the maker's done editing live, the draft
// is now the source of truth until a reviewer decides.
onClose()
}}
/>