feat(my-drafts): schema drafts тоже в "Мои черновики"
This commit is contained in:
@@ -290,6 +290,27 @@ export const schemaReviewQueueQuery = (page = 0, size = 50) =>
|
||||
export const useSchemaReviewQueue = (page = 0, size = 50) =>
|
||||
useQuery(schemaReviewQueueQuery(page, size))
|
||||
|
||||
/**
|
||||
* Maker self-tracking — все мои schema drafts (любой статус). Зеркало
|
||||
* `myDraftsQuery` для record drafts. Frontend объединяет обе очереди
|
||||
* на /my-drafts, чтобы пользователь не запоминал какой тип где.
|
||||
*/
|
||||
export const mySchemaDraftsQuery = (page = 0, size = 50) =>
|
||||
queryOptions({
|
||||
queryKey: ['schema-drafts', 'me', page, size] as const,
|
||||
queryFn: async (): Promise<SchemaReviewQueuePage> => {
|
||||
const { data } = await apiClient.get<SchemaReviewQueuePage>(
|
||||
'/admin/schema-drafts/me',
|
||||
{ params: { page, size } },
|
||||
)
|
||||
return data
|
||||
},
|
||||
refetchInterval: 30_000,
|
||||
})
|
||||
|
||||
export const useMySchemaDrafts = (page = 0, size = 50) =>
|
||||
useQuery(mySchemaDraftsQuery(page, size))
|
||||
|
||||
export const recordRawQuery = (dictionaryName: string, businessKey: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['record-raw', dictionaryName, businessKey] as const,
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
import {
|
||||
useDictionaries,
|
||||
useMyDrafts,
|
||||
useMySchemaDrafts,
|
||||
useOutboxStats,
|
||||
useReviewsBadgeCount,
|
||||
useWebhookSubscriptions,
|
||||
@@ -76,7 +77,10 @@ export function Sidebar() {
|
||||
const isReviewer = canReviewSchema || canReviewRecord
|
||||
const dictsQuery = useDictionaries()
|
||||
// Loaded just for сидбар counter — light request, кэшируется.
|
||||
// Badge "Мои черновики" суммирует record (/drafts/me) + schema
|
||||
// (/admin/schema-drafts/me) — maker не должен помнить разные типы.
|
||||
const myDrafts = useMyDrafts(0, 1)
|
||||
const mySchemaDrafts = useMySchemaDrafts(0, 1)
|
||||
// Reviews badge — суммирует record-reviews + schema-reviews pending queues
|
||||
// через size=1 paged запросы. Gated на authenticated.
|
||||
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
|
||||
@@ -86,7 +90,8 @@ export function Sidebar() {
|
||||
const webhooks = useWebhookSubscriptions()
|
||||
|
||||
const dictsCount = dictsQuery.data?.length ?? 0
|
||||
const myDraftsCount = myDrafts.data?.totalElements ?? 0
|
||||
const myDraftsCount =
|
||||
(myDrafts.data?.totalElements ?? 0) + (mySchemaDrafts.data?.totalElements ?? 0)
|
||||
const reviewsCount = reviews.total
|
||||
// Pending events ждут dispatch. dlq отдельно — alarming, но не блокирует
|
||||
// нормальную работу, можно увидеть на /outbox. Badge показывает только
|
||||
@@ -444,12 +449,14 @@ export function MobileSidebar({ open, onClose }: { open: boolean; onClose: () =>
|
||||
const isReviewer = canReviewSchema || canReviewRecord
|
||||
const dictsQuery = useDictionaries()
|
||||
const myDrafts = useMyDrafts(0, 1)
|
||||
const mySchemaDrafts = useMySchemaDrafts(0, 1)
|
||||
const reviews = useReviewsBadgeCount(auth.isAuthenticated)
|
||||
const outboxStats = useOutboxStats()
|
||||
const webhooks = useWebhookSubscriptions()
|
||||
|
||||
const dictsCount = dictsQuery.data?.length ?? 0
|
||||
const myDraftsCount = myDrafts.data?.totalElements ?? 0
|
||||
const myDraftsCount =
|
||||
(myDrafts.data?.totalElements ?? 0) + (mySchemaDrafts.data?.totalElements ?? 0)
|
||||
const reviewsCount = reviews.total
|
||||
const outboxPending = outboxStats.data?.pending ?? 0
|
||||
const webhooksActive =
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { Link, createFileRoute } from '@tanstack/react-router'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import {
|
||||
Alert,
|
||||
@@ -16,9 +16,9 @@ import {
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
} from '@/ui'
|
||||
import { useMyDrafts } from '@/api/queries'
|
||||
import { useMyDrafts, useMySchemaDrafts } from '@/api/queries'
|
||||
import { useWithdrawDraft } from '@/api/mutations'
|
||||
import type { DraftOperation, DraftStatus } from '@/api/client'
|
||||
import type { DraftOperation, DraftStatus, SchemaDraft } from '@/api/client'
|
||||
|
||||
export const Route = createFileRoute('/my-drafts')({
|
||||
component: MyDraftsPage,
|
||||
@@ -39,6 +39,17 @@ const statusVariant = (
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
/** Mapping schema draft статусов на UI badge variant. */
|
||||
const schemaStatusVariant = (
|
||||
s: string,
|
||||
): 'warning' | 'success' | 'error' | 'info' | 'neutral' => {
|
||||
if (s === 'review_pending') return 'warning'
|
||||
if (s === 'approved' || s === 'published') return 'success'
|
||||
if (s === 'rejected') return 'error'
|
||||
if (s === 'changes_requested') return 'info'
|
||||
return 'neutral'
|
||||
}
|
||||
|
||||
/**
|
||||
* Approval Workflow v2 Phase 4: maker self-tracking. Maker видит свои
|
||||
* submissions всех статусов (PENDING/APPROVED/REJECTED/WITHDRAWN), sorted
|
||||
@@ -49,6 +60,7 @@ const statusVariant = (
|
||||
function MyDraftsPage() {
|
||||
const { t } = useTranslation()
|
||||
const drafts = useMyDrafts(0, 50)
|
||||
const schemaDrafts = useMySchemaDrafts(0, 50)
|
||||
const withdrawMut = useWithdrawDraft()
|
||||
const [busyId, setBusyId] = useState<string | undefined>(undefined)
|
||||
|
||||
@@ -60,6 +72,14 @@ function MyDraftsPage() {
|
||||
})
|
||||
}
|
||||
|
||||
const recordTotal = drafts.data?.totalElements ?? 0
|
||||
const schemaTotal = schemaDrafts.data?.totalElements ?? 0
|
||||
const bothEmpty =
|
||||
!drafts.isLoading &&
|
||||
!schemaDrafts.isLoading &&
|
||||
recordTotal === 0 &&
|
||||
schemaTotal === 0
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<PageHeader
|
||||
@@ -67,24 +87,38 @@ function MyDraftsPage() {
|
||||
description={t('myDrafts.description')}
|
||||
/>
|
||||
|
||||
{drafts.isLoading && <LoadingBlock size="md" label={t('loading')} />}
|
||||
{(drafts.isLoading || schemaDrafts.isLoading) && (
|
||||
<LoadingBlock size="md" label={t('loading')} />
|
||||
)}
|
||||
|
||||
{drafts.error && (
|
||||
<Alert variant="error" title={t('error.failed')}>
|
||||
{String(drafts.error)}
|
||||
</Alert>
|
||||
)}
|
||||
{schemaDrafts.error && (
|
||||
<Alert variant="error" title={t('error.failed')}>
|
||||
{String(schemaDrafts.error)}
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{drafts.data && drafts.data.totalElements === 0 && (
|
||||
<EmptyState title={t('myDrafts.empty')} />
|
||||
{bothEmpty && <EmptyState title={t('myDrafts.empty')} />}
|
||||
|
||||
{schemaTotal > 0 && (
|
||||
<MySchemaDraftsPanel items={(schemaDrafts.data?.items ?? []) as SchemaDraft[]} total={schemaTotal} />
|
||||
)}
|
||||
|
||||
{drafts.data && drafts.data.totalElements > 0 && (
|
||||
<Panel>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<p className="text-cell text-mute">
|
||||
{t('myDrafts.total', { count: drafts.data.totalElements })}
|
||||
</p>
|
||||
<div>
|
||||
<h2 className="text-title-md font-semibold">
|
||||
{t('myDrafts.records.title', { defaultValue: 'Записи' })}
|
||||
</h2>
|
||||
<p className="text-cell text-mute mt-0.5">
|
||||
{t('myDrafts.total', { count: drafts.data.totalElements })}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
@@ -146,3 +180,96 @@ function MyDraftsPage() {
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Schema drafts maker self-tracking panel. Параллельно с record drafts
|
||||
* на той же странице — пользователь не должен помнить разные типы.
|
||||
*
|
||||
* <p>Status badge показывает schema state machine (draft / review_pending /
|
||||
* approved / rejected / changes_requested / withdrawn / published).
|
||||
* "Открыть" линкует на dictionary page где есть drawer с details + actions
|
||||
* (withdraw / re-submit для maker).
|
||||
*/
|
||||
function MySchemaDraftsPanel({ items, total }: { items: SchemaDraft[]; total: number }) {
|
||||
const { t } = useTranslation()
|
||||
return (
|
||||
<Panel>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div>
|
||||
<h2 className="text-title-md font-semibold">
|
||||
{t('myDrafts.schema.title', { defaultValue: 'Черновики схем' })}
|
||||
</h2>
|
||||
<p className="text-cell text-mute mt-0.5">
|
||||
{t('myDrafts.schema.total', {
|
||||
count: total,
|
||||
defaultValue: `${total} schema draft(s)`,
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<Badge variant="info">
|
||||
{t('myDrafts.schema.label', { defaultValue: 'Schema workflow' })}
|
||||
</Badge>
|
||||
</div>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>
|
||||
{t('myDrafts.schema.col.dictionary', { defaultValue: 'Словарь' })}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
{t('myDrafts.schema.col.fromVersion', { defaultValue: 'От версии' })}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
{t('myDrafts.schema.col.status', { defaultValue: 'Статус' })}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
{t('myDrafts.schema.col.created', { defaultValue: 'Создан' })}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
{t('myDrafts.schema.col.submitted', { defaultValue: 'Отправлен' })}
|
||||
</TableHeaderCell>
|
||||
<TableHeaderCell align="right">
|
||||
{t('myDrafts.schema.col.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>
|
||||
<Badge variant={schemaStatusVariant(d.status)}>{d.status}</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="tabular-nums">
|
||||
{new Date(d.createdAt).toLocaleString()}
|
||||
</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('myDrafts.schema.open', { defaultValue: 'Открыть' })} →
|
||||
</Link>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Panel>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user