feat(schema-workflow): Phase 1b — SchemaDraftDrawer with status-aware actions
This commit is contained in:
@@ -1,8 +1,11 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Link } from '@tanstack/react-router'
|
||||
import { ClipboardList, FileText } from 'lucide-react'
|
||||
import type { DictionaryDetail } from '@/api/client'
|
||||
import { ClipboardList, FileText, GitBranch } from 'lucide-react'
|
||||
import type { DictionaryDetail, SchemaDraft, SchemaDraftStatus } from '@/api/client'
|
||||
import { useDictActiveSchemaDraft } from '@/api/queries'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SchemaDraftDrawer } from '@/components/workflow/SchemaDraftDrawer'
|
||||
|
||||
/**
|
||||
* WorkflowBanner — status banner для editor (Screen 2 per handoff).
|
||||
@@ -32,8 +35,18 @@ export type WorkflowBannerProps = {
|
||||
|
||||
export function WorkflowBanner({ detail, pendingCount = 0 }: WorkflowBannerProps) {
|
||||
const { t } = useTranslation()
|
||||
// Phase 1a: read-side active schema draft. Если на dict'е есть
|
||||
// non-terminal schema draft — surface'им его state выше record-approval
|
||||
// banner'а (schema draft = higher salience: меняет структуру всего dict'а,
|
||||
// record approval — per-row).
|
||||
const { data: schemaDraft } = useDictActiveSchemaDraft(detail.name)
|
||||
|
||||
// === Case 1: Live — no approval required ===
|
||||
// === Case A: Active schema draft — highest priority ===
|
||||
if (schemaDraft) {
|
||||
return <SchemaDraftBanner draft={schemaDraft} detail={detail} />
|
||||
}
|
||||
|
||||
// === Case 1: Live — no approval required (and no schema draft) ===
|
||||
// Постоянный "Опубликовано" banner = шум. Status уже виден в InfoPanel
|
||||
// (scope, version, updatedAt). Не показываем banner вообще.
|
||||
if (!detail.approvalRequired) {
|
||||
@@ -102,6 +115,94 @@ const VARIANT_CLASS: Record<BannerProps['variant'], string> = {
|
||||
draft: 'bg-warn-bg border-l-warn text-ink',
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema-level draft banner — surface'им active non-terminal draft каждому
|
||||
* пользователю который заходит в editor словаря. Tone зависит от status'а:
|
||||
*
|
||||
* <ul>
|
||||
* <li>draft / changes_requested — warn (maker должен дойти и submit'нуть).</li>
|
||||
* <li>review_pending — info (approvers watching, maker waits).</li>
|
||||
* <li>approved — review (готов к publish, кто-то с publish_role нажмёт).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>Phase 1a: read-only banner. Кнопки «Отправить», «Approve», «Publish»
|
||||
* проводным появятся в Phase 1b (модалки + RBAC role check). Сейчас просто
|
||||
* link на draft detail (когда такая страница появится — placeholder).
|
||||
*/
|
||||
function SchemaDraftBanner({
|
||||
draft,
|
||||
detail,
|
||||
}: {
|
||||
draft: SchemaDraft
|
||||
detail: DictionaryDetail
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
const variant = schemaDraftVariant(draft.status)
|
||||
const labelKey = `workflow.schemaDraft.${draft.status}`
|
||||
const fallbackLabel: Record<SchemaDraftStatus, string> = {
|
||||
draft: 'Черновик схемы создан',
|
||||
review_pending: 'Черновик схемы на ревью',
|
||||
approved: 'Черновик схемы одобрен',
|
||||
changes_requested: 'По черновику запрошены правки',
|
||||
rejected: 'Черновик схемы отклонён',
|
||||
published: 'Черновик опубликован',
|
||||
withdrawn: 'Черновик отозван',
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Banner
|
||||
variant={variant}
|
||||
icon={<GitBranch size={16} strokeWidth={2} className="shrink-0" />}
|
||||
action={
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setDrawerOpen(true)}
|
||||
className="text-body font-medium text-accent hover:underline shrink-0 cursor-pointer"
|
||||
>
|
||||
{t('workflow.schemaDraft.open', { defaultValue: 'Открыть черновик' })} →
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<span className="font-medium">
|
||||
{t(labelKey, { defaultValue: fallbackLabel[draft.status] })}
|
||||
</span>
|
||||
<span className="text-cell opacity-70">
|
||||
{t('workflow.schemaDraft.subtitle', {
|
||||
fromVersion: draft.branchedFromVersion,
|
||||
defaultValue: `от v${draft.branchedFromVersion}`,
|
||||
})}
|
||||
</span>
|
||||
</Banner>
|
||||
<SchemaDraftDrawer
|
||||
open={drawerOpen}
|
||||
onClose={() => setDrawerOpen(false)}
|
||||
dictionaryName={detail.name}
|
||||
currentHeadVersion={detail.schemaVersion}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function schemaDraftVariant(status: SchemaDraftStatus): BannerProps['variant'] {
|
||||
switch (status) {
|
||||
case 'draft':
|
||||
case 'changes_requested':
|
||||
return 'draft'
|
||||
case 'review_pending':
|
||||
return 'review'
|
||||
case 'approved':
|
||||
return 'live'
|
||||
case 'rejected':
|
||||
case 'withdrawn':
|
||||
case 'published':
|
||||
// Terminal states обычно не должны попасть сюда (useDictActiveSchemaDraft
|
||||
// фильтрует non-terminal), но если backend пометил draft active'ом —
|
||||
// показываем neutral info.
|
||||
return 'info'
|
||||
}
|
||||
}
|
||||
|
||||
function Banner({ variant, icon, action, children }: BannerProps) {
|
||||
return (
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user