feat(schema-workflow): Phase 1c — CreateModal + /reviews extension + RBAC stub
This commit is contained in:
@@ -25,10 +25,11 @@ import {
|
||||
TableRow,
|
||||
TextInput,
|
||||
} from '@/ui'
|
||||
import { PlusIcon, PencilSimpleIcon, XCircleIcon, GearIcon, ClockCounterClockwiseIcon, TrashIcon, CheckCircleIcon, WarningIcon, XIcon, CaretLeftIcon, CaretRightIcon, DotsThreeVerticalIcon } from '@phosphor-icons/react'
|
||||
import { PlusIcon, PencilSimpleIcon, XCircleIcon, GearIcon, ClockCounterClockwiseIcon, TrashIcon, CheckCircleIcon, WarningIcon, XIcon, CaretLeftIcon, CaretRightIcon, DotsThreeVerticalIcon, GitBranchIcon } from '@phosphor-icons/react'
|
||||
import {
|
||||
useAudit,
|
||||
useChangelog,
|
||||
useDictActiveSchemaDraft,
|
||||
useDictionaryDetail,
|
||||
useDictPendingDrafts,
|
||||
useRecordRaw,
|
||||
@@ -53,6 +54,7 @@ import { AoiPickerDialog, type AoiResult } from '@/components/geo/AoiPickerDialo
|
||||
import { TimeTravelModal } from '@/components/timetravel/TimeTravelModal'
|
||||
import { ChangelogDiffModal } from '@/components/changelog/ChangelogDiffModal'
|
||||
import { kindBadgeVariant, kindIcon, kindLabel, isVersioningKind } from '@/components/changelog/kind-meta'
|
||||
import { CreateSchemaDraftModal } from '@/components/workflow/CreateSchemaDraftModal'
|
||||
import { nowIsoLocal } from '@/lib/dates'
|
||||
import { recordDisplayName } from '@/lib/locales'
|
||||
import { SCOPE_BORDER_TOP } from '@/lib/scope-style'
|
||||
@@ -132,6 +134,14 @@ function DictionaryDetail() {
|
||||
const { t } = useTranslation()
|
||||
const detailQuery = useDictionaryDetail(name)
|
||||
const canMutate = useCanMutate()
|
||||
// Phase 1c: show «Create draft» button only когда approvalRequired=true
|
||||
// и нет active draft'а (иначе banner предложит open existing). Live (без
|
||||
// approval flag) словари редактируются напрямую через EditorDialog —
|
||||
// создавать draft не нужно.
|
||||
const activeSchemaDraftQuery = useDictActiveSchemaDraft(name)
|
||||
const shouldOfferCreateDraft =
|
||||
detailQuery.data?.approvalRequired === true &&
|
||||
!activeSchemaDraftQuery.data
|
||||
|
||||
// AOI: bbox из URL → bbox-AOI rendered initially; polygon AOI живёт
|
||||
// только в local state (не сериализуем в URL — слишком длинно).
|
||||
@@ -194,6 +204,7 @@ function DictionaryDetail() {
|
||||
total: 0,
|
||||
})
|
||||
const [schemaEditOpen, setSchemaEditOpen] = useState(false)
|
||||
const [createDraftOpen, setCreateDraftOpen] = useState(false)
|
||||
const [historyKey, setHistoryKey] = useState<string | undefined>(undefined)
|
||||
const [aoiOpen, setAoiOpen] = useState(false)
|
||||
const [timeTravelOpen, setTimeTravelOpen] = useState(false)
|
||||
@@ -703,6 +714,21 @@ function DictionaryDetail() {
|
||||
actions={
|
||||
canMutate ? (
|
||||
<>
|
||||
{/* approval-required dict без active draft: предлагаем
|
||||
создать черновик. Direct edit через GearIcon оставляем
|
||||
для админов, но primary path — через workflow. */}
|
||||
{shouldOfferCreateDraft && (
|
||||
<IconButton
|
||||
type="button"
|
||||
label={t('createDraft.openButton', {
|
||||
defaultValue: 'Создать черновик схемы',
|
||||
})}
|
||||
variant="default"
|
||||
icon={<GitBranchIcon weight="regular" />}
|
||||
disabled={!detailQuery.data}
|
||||
onClick={() => setCreateDraftOpen(true)}
|
||||
/>
|
||||
)}
|
||||
<IconButton
|
||||
type="button"
|
||||
label={t('schema.action.editSchema')}
|
||||
@@ -1214,6 +1240,14 @@ function DictionaryDetail() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{detailQuery.data && (
|
||||
<CreateSchemaDraftModal
|
||||
open={createDraftOpen}
|
||||
onClose={() => setCreateDraftOpen(false)}
|
||||
detail={detailQuery.data}
|
||||
/>
|
||||
)}
|
||||
|
||||
<RecordHistoryDrawer
|
||||
open={Boolean(historyKey)}
|
||||
onClose={() => setHistoryKey(undefined)}
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
TextInput,
|
||||
} from '@/ui'
|
||||
import { CheckCircleIcon, XCircleIcon } from '@phosphor-icons/react'
|
||||
import { useDraft, useLiveRecord, useReviewQueue } from '@/api/queries'
|
||||
import { useDraft, useLiveRecord, useReviewQueue, useSchemaReviewQueue } from '@/api/queries'
|
||||
import { useApproveDraft, useRejectDraft } from '@/api/mutations'
|
||||
import type { DraftOperation, DraftResponse } from '@/api/client'
|
||||
|
||||
@@ -142,6 +142,8 @@ function ReviewsPage() {
|
||||
description={t('reviews.description')}
|
||||
/>
|
||||
|
||||
<SchemaReviewQueuePanel />
|
||||
|
||||
{queue.isLoading && <LoadingBlock size="md" label={t('loading')} />}
|
||||
|
||||
{queue.error && (
|
||||
@@ -507,3 +509,80 @@ function ReviewDrawer({ draftId, onClose }: DrawerProps) {
|
||||
</Drawer>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema review pool queue — global view всех \`review_pending\` schema
|
||||
* draft'ов через {@code GET /admin/schema-reviews/pending}. Reviewer
|
||||
* жмёт «Открыть» → переходит на dict detail page, banner+drawer
|
||||
* автоматически подхватятся через {@code useDictActiveSchemaDraft}.
|
||||
*
|
||||
* <p>Скрывается если pool пуст — экономит вертикальное место на странице
|
||||
* для основного record-review queue.
|
||||
*/
|
||||
function SchemaReviewQueuePanel() {
|
||||
const { t } = useTranslation()
|
||||
const queue = useSchemaReviewQueue(0, 20)
|
||||
const items = queue.data?.items ?? []
|
||||
if (queue.isLoading || items.length === 0) return null
|
||||
|
||||
return (
|
||||
<Panel>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h2 className="text-title-md font-semibold">
|
||||
{t('reviews.schema.title', { defaultValue: 'Изменения схем' })}
|
||||
</h2>
|
||||
<p className="text-cell text-mute mt-0.5">
|
||||
{t('reviews.schema.queueTotal', {
|
||||
count: queue.data?.total ?? items.length,
|
||||
defaultValue: `${queue.data?.total ?? items.length} draft(s) ждут ревью`,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="info">
|
||||
{t('reviews.schema.label', { defaultValue: 'Schema workflow' })}
|
||||
</Badge>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>{t('reviews.schema.dictionary', { defaultValue: 'Словарь' })}</TableHeaderCell>
|
||||
<TableHeaderCell>{t('reviews.schema.fromVersion', { defaultValue: 'От версии' })}</TableHeaderCell>
|
||||
<TableHeaderCell>{t('reviews.schema.maker', { defaultValue: 'Автор' })}</TableHeaderCell>
|
||||
<TableHeaderCell>{t('reviews.schema.submittedAt', { defaultValue: 'Отправлен' })}</TableHeaderCell>
|
||||
<TableHeaderCell align="right">{t('reviews.schema.action', { defaultValue: 'Действие' })}</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{items.map((d) => (
|
||||
<TableRow key={d.draftId}>
|
||||
<TableCell>
|
||||
<Link
|
||||
to="/dictionaries/$name"
|
||||
params={{ name: d.dictionaryName }}
|
||||
className="text-accent hover:underline font-mono"
|
||||
>
|
||||
{d.dictionaryName}
|
||||
</Link>
|
||||
</TableCell>
|
||||
<TableCell className="font-mono tabular-nums">v{d.branchedFromVersion}</TableCell>
|
||||
<TableCell className="font-mono">{d.makerId}</TableCell>
|
||||
<TableCell className="tabular-nums">
|
||||
{d.submittedAt ? new Date(d.submittedAt).toLocaleString() : '—'}
|
||||
</TableCell>
|
||||
<TableCell align="right">
|
||||
<Link
|
||||
to="/dictionaries/$name"
|
||||
params={{ name: d.dictionaryName }}
|
||||
className="text-accent hover:underline"
|
||||
>
|
||||
{t('reviews.schema.open', { defaultValue: 'Открыть' })} →
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user