From f096b5746d256220f569616813b8ec886b27247d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=97=D0=B8=D0=BC=D0=B8=D0=BD?= Date: Wed, 13 May 2026 12:45:45 +0000 Subject: [PATCH] =?UTF-8?q?feat(my-drafts):=20schema=20drafts=20=D1=82?= =?UTF-8?q?=D0=BE=D0=B6=D0=B5=20=D0=B2=20"=D0=9C=D0=BE=D0=B8=20=D1=87?= =?UTF-8?q?=D0=B5=D1=80=D0=BD=D0=BE=D0=B2=D0=B8=D0=BA=D0=B8"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ordinis-admin-ui/src/api/queries.ts | 21 +++ .../src/components/layout/Sidebar.tsx | 11 +- ordinis-admin-ui/src/routes/my-drafts.tsx | 145 ++++++++++++++++-- .../schemaworkflow/SchemaDraftService.java | 14 ++ .../restapi/web/SchemaDraftController.java | 38 +++++ 5 files changed, 218 insertions(+), 11 deletions(-) diff --git a/ordinis-admin-ui/src/api/queries.ts b/ordinis-admin-ui/src/api/queries.ts index a89817c..e79ae67 100644 --- a/ordinis-admin-ui/src/api/queries.ts +++ b/ordinis-admin-ui/src/api/queries.ts @@ -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 => { + const { data } = await apiClient.get( + '/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, diff --git a/ordinis-admin-ui/src/components/layout/Sidebar.tsx b/ordinis-admin-ui/src/components/layout/Sidebar.tsx index 5664f80..e821a53 100644 --- a/ordinis-admin-ui/src/components/layout/Sidebar.tsx +++ b/ordinis-admin-ui/src/components/layout/Sidebar.tsx @@ -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 = diff --git a/ordinis-admin-ui/src/routes/my-drafts.tsx b/ordinis-admin-ui/src/routes/my-drafts.tsx index f55dd90..f4617cd 100644 --- a/ordinis-admin-ui/src/routes/my-drafts.tsx +++ b/ordinis-admin-ui/src/routes/my-drafts.tsx @@ -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(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 (
- {drafts.isLoading && } + {(drafts.isLoading || schemaDrafts.isLoading) && ( + + )} {drafts.error && ( {String(drafts.error)} )} + {schemaDrafts.error && ( + + {String(schemaDrafts.error)} + + )} - {drafts.data && drafts.data.totalElements === 0 && ( - + {bothEmpty && } + + {schemaTotal > 0 && ( + )} {drafts.data && drafts.data.totalElements > 0 && (
-

- {t('myDrafts.total', { count: drafts.data.totalElements })} -

+
+

+ {t('myDrafts.records.title', { defaultValue: 'Записи' })} +

+

+ {t('myDrafts.total', { count: drafts.data.totalElements })} +

+
@@ -146,3 +180,96 @@ function MyDraftsPage() { ) } + +/** + * Schema drafts maker self-tracking panel. Параллельно с record drafts + * на той же странице — пользователь не должен помнить разные типы. + * + *

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 ( + +

+
+

+ {t('myDrafts.schema.title', { defaultValue: 'Черновики схем' })} +

+

+ {t('myDrafts.schema.total', { + count: total, + defaultValue: `${total} schema draft(s)`, + })} +

+
+ + {t('myDrafts.schema.label', { defaultValue: 'Schema workflow' })} + +
+
+ + + + {t('myDrafts.schema.col.dictionary', { defaultValue: 'Словарь' })} + + + {t('myDrafts.schema.col.fromVersion', { defaultValue: 'От версии' })} + + + {t('myDrafts.schema.col.status', { defaultValue: 'Статус' })} + + + {t('myDrafts.schema.col.created', { defaultValue: 'Создан' })} + + + {t('myDrafts.schema.col.submitted', { defaultValue: 'Отправлен' })} + + + {t('myDrafts.schema.col.action', { defaultValue: 'Действие' })} + + + + + {items.map((d) => ( + + + + {d.dictionaryName ?? '—'} + + + + v{d.branchedFromVersion} + + + {d.status} + + + {new Date(d.createdAt).toLocaleString()} + + + {d.submittedAt ? new Date(d.submittedAt).toLocaleString() : '—'} + + + + {t('myDrafts.schema.open', { defaultValue: 'Открыть' })} → + + + + ))} + +
+
+ ) +} diff --git a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/service/schemaworkflow/SchemaDraftService.java b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/service/schemaworkflow/SchemaDraftService.java index deb3602..098db4b 100644 --- a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/service/schemaworkflow/SchemaDraftService.java +++ b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/service/schemaworkflow/SchemaDraftService.java @@ -130,6 +130,20 @@ public class SchemaDraftService { return repository.findPendingReview(pageable); } + /** + * Maker self-tracking — все schema drafts которые создал actor, любой + * статус (DRAFT / REVIEW_PENDING / APPROVED / REJECTED / CHANGES_REQUESTED + * / WITHDRAWN / PUBLISHED), отсортированы createdAt DESC. + * + *

Зеркало record-side {@code DraftService.listByMaker} — frontend + * "Мои черновики" (route {@code /my-drafts}) показывает оба типа, + * чтобы maker не запоминал где какие drafts лежат. + */ + @Transactional(readOnly = true) + public Page listByMaker(String makerId, Pageable pageable) { + return repository.findByMaker(makerId, pageable); + } + // ───────────────────────────────────────────────────────────────────── // Mutators (state machine transitions) // ───────────────────────────────────────────────────────────────────── diff --git a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/SchemaDraftController.java b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/SchemaDraftController.java index e5ff9b2..ea583d1 100644 --- a/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/SchemaDraftController.java +++ b/ordinis-rest-api/src/main/java/cloud/nstart/terravault/ordinis/restapi/web/SchemaDraftController.java @@ -41,6 +41,7 @@ import java.util.UUID; * GET /api/v1/dictionaries/{name}/drafts/active * GET /api/v1/dictionaries/{name}/drafts/{draftId} * GET /api/v1/admin/schema-reviews/pending (pool queue) + * GET /api/v1/admin/schema-drafts/me (maker tracking) * * *

{@code {name}} в path передаётся для красивых URL'ов + кэширования @@ -211,4 +212,41 @@ public class SchemaDraftController { "totalElements", pg.getTotalElements(), "totalPages", pg.getTotalPages()); } + + /** + * Maker self-tracking — все schema drafts актора, любой статус. + * Зеркало record-side {@code /api/v1/drafts/me}. Frontend "Мои черновики" + * объединяет обе очереди (record + schema), чтобы пользователь не + * запоминал какой тип где лежит. + * + *

Sorted createdAt DESC (newest first) — самые свежие drafts наверху, + * legacy / approved уходят вниз. UI добавляет фильтр по status. + */ + @GetMapping("/api/v1/admin/schema-drafts/me") + public Map myDrafts( + @RequestParam(defaultValue = "0") int page, + @RequestParam(defaultValue = "50") int size) { + String makerId = currentUserId(); + var pg = draftService.listByMaker(makerId, PageRequest.of(page, Math.min(size, 200))); + List items = pg.getContent().stream() + .map(d -> { + DictionaryDefinition def = definitionService.findById(d.getDictionaryId()); + return SchemaDraftResponse.from(d, def.getName()); + }) + .toList(); + return Map.of( + "items", items, + "page", pg.getNumber(), + "size", pg.getSize(), + "totalElements", pg.getTotalElements(), + "totalPages", pg.getTotalPages()); + } + + /** Resolve current user from JWT subject. Fallback "anonymous" если auth off. */ + private static String currentUserId() { + var auth = org.springframework.security.core.context.SecurityContextHolder + .getContext().getAuthentication(); + if (auth == null || !auth.isAuthenticated()) return "anonymous"; + return auth.getName(); + } }