Merge branch 'feat/scheduled-records-hint' into 'main'
feat(records): scheduled records empty-state hint See merge request 2-6/2-6-4/terravault/ordinis!222
This commit is contained in:
@@ -756,3 +756,18 @@ export const NOTIFICATION_EVENT_TYPES = [
|
||||
'RecordDraftWithdrawn',
|
||||
] as const
|
||||
export type NotificationEventType = (typeof NOTIFICATION_EVENT_TYPES)[number]
|
||||
|
||||
/**
|
||||
* Empty-state hint payload (read-api scheduled-summary). Подсчёт записей с
|
||||
* {@code validFrom > now AND validTo > now} в текущем scope view.
|
||||
*
|
||||
* <ul>
|
||||
* <li>{@code count=0} → frontend hint не показывает.</li>
|
||||
* <li>{@code nearestValidFrom} — ISO datetime ближайшей запланированной;
|
||||
* используется в CTA «Перейти к дате» (time-travel).</li>
|
||||
* </ul>
|
||||
*/
|
||||
export type ScheduledRecordsSummary = {
|
||||
count: number
|
||||
nearestValidFrom: string | null
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
type RecordDependentsPage,
|
||||
type RecordResponse,
|
||||
type ReviewQueuePage,
|
||||
type ScheduledRecordsSummary,
|
||||
type SchemaDependent,
|
||||
type SearchResponse,
|
||||
type WebhookDeliveryPage,
|
||||
@@ -784,6 +785,37 @@ export const useRecords = (
|
||||
scopeCsv: string,
|
||||
filter?: RecordsFilter,
|
||||
) => useQuery(recordsQuery(dictionaryName, scopeCsv, filter))
|
||||
|
||||
/**
|
||||
* Scheduled records hint для empty-state. Fetch'ит count записей с
|
||||
* validFrom > now (но validTo > now — т.е. не сразу закрытые). Показывается
|
||||
* пользователю когда список пуст, но в будущем что-то запланировано.
|
||||
*
|
||||
* <p>Fixes UX bug: user создал запись с validFrom=18.05, открыл catalog 15.05 —
|
||||
* пустая таблица, никаких хинтов. Теперь UI показывает «Запланировано: 1
|
||||
* запись на 18.05 [перейти к дате]».
|
||||
*
|
||||
* <p>Stale 30s — scheduled state changes медленно. Refetch on focus = true
|
||||
* (default) — пользователь возвращается во вкладку → fresh count.
|
||||
*/
|
||||
export const scheduledSummaryQuery = (dictionaryName: string, scopeCsv: string) =>
|
||||
queryOptions({
|
||||
queryKey: ['scheduled-summary', dictionaryName, scopeCsv] as const,
|
||||
queryFn: async (): Promise<ScheduledRecordsSummary> => {
|
||||
const { data } = await apiClient.get<ScheduledRecordsSummary>(
|
||||
`/${dictionaryName}/records/scheduled-summary`,
|
||||
{ params: { as_scope: scopeCsv } },
|
||||
)
|
||||
return data
|
||||
},
|
||||
staleTime: 30_000,
|
||||
})
|
||||
|
||||
export const useScheduledSummary = (dictionaryName: string, scopeCsv: string) =>
|
||||
useQuery({
|
||||
...scheduledSummaryQuery(dictionaryName, scopeCsv),
|
||||
enabled: Boolean(dictionaryName),
|
||||
})
|
||||
export const useRecordRaw = (
|
||||
dictionaryName: string,
|
||||
businessKey: string | undefined,
|
||||
|
||||
@@ -349,6 +349,15 @@ i18n
|
||||
'dict.list.fk.usedBy_many': '← {{count}} использ.',
|
||||
'dict.list.fk.usedBy_other': '← {{count}} использ.',
|
||||
'dict.empty': 'В этом справочнике пока нет записей',
|
||||
'dict.scheduled.hint_one':
|
||||
'Запланирована {{count}} запись на {{date}}',
|
||||
'dict.scheduled.hint_few':
|
||||
'Запланировано {{count}} записи, ближайшая на {{date}}',
|
||||
'dict.scheduled.hint_many':
|
||||
'Запланировано {{count}} записей, ближайшая на {{date}}',
|
||||
'dict.scheduled.hint_other':
|
||||
'Запланировано {{count}} записей, ближайшая на {{date}}',
|
||||
'dict.scheduled.cta': 'Перейти к этой дате',
|
||||
'dict.col.businessKey': 'Бизнес-ключ',
|
||||
'dict.col.scope': 'Scope',
|
||||
'dict.col.validFrom': 'Действует с',
|
||||
@@ -1151,6 +1160,11 @@ i18n
|
||||
'dict.list.fk.usedBy_one': '← {{count}} used by',
|
||||
'dict.list.fk.usedBy_other': '← {{count}} used by',
|
||||
'dict.empty': 'No records in this dictionary yet',
|
||||
'dict.scheduled.hint_one':
|
||||
'{{count}} record scheduled for {{date}}',
|
||||
'dict.scheduled.hint_other':
|
||||
'{{count}} records scheduled, earliest on {{date}}',
|
||||
'dict.scheduled.cta': 'Jump to that date',
|
||||
'dict.col.businessKey': 'Business key',
|
||||
'dict.col.scope': 'Scope',
|
||||
'dict.col.validFrom': 'Valid from',
|
||||
|
||||
@@ -35,6 +35,7 @@ import {
|
||||
useDictPendingDrafts,
|
||||
useRecordRaw,
|
||||
useRecords,
|
||||
useScheduledSummary,
|
||||
type RecordsFilter,
|
||||
} from '@/api/queries'
|
||||
import { useBulkCloseRecords, useBulkExportRecords, useCreateRecord, useUpdateRecord, useCloseRecord, useFormIdempotencyKey, useSubmitDraft } from '@/api/mutations'
|
||||
@@ -169,6 +170,16 @@ function DictionaryDetail() {
|
||||
}
|
||||
: undefined
|
||||
const recordsResult = useRecords(name, 'PUBLIC,INTERNAL,RESTRICTED', filter)
|
||||
/**
|
||||
* Scheduled records hint (post-Phase-B-2 UX fix). Когда records list пустой
|
||||
* на момент now / time-travel, но в БД есть записи с validFrom > now,
|
||||
* показываем «Запланировано N, ближайшая на DD.MM [Перейти к этой дате]».
|
||||
* Fixes user feedback: «создал запись на 18.05 — её не видно».
|
||||
*
|
||||
* <p>Fetch unconditional (cheap single-aggregate query). Frontend сам решает
|
||||
* показывать hint или нет по {@code count > 0}.
|
||||
*/
|
||||
const scheduledSummary = useScheduledSummary(name, 'PUBLIC,INTERNAL,RESTRICTED')
|
||||
/**
|
||||
* Approval Workflow v2 Phase 4: pending drafts на этот dict — для бейджа
|
||||
* "На review" в каждой строке records table. Polled 30s. Bypass'нем если
|
||||
@@ -974,7 +985,30 @@ function DictionaryDetail() {
|
||||
)}
|
||||
|
||||
{recordsResult.data && recordsResult.data.length === 0 && (
|
||||
<EmptyState title={t('dict.empty')} />
|
||||
<EmptyState
|
||||
title={t('dict.empty')}
|
||||
description={
|
||||
scheduledSummary.data && scheduledSummary.data.count > 0 && scheduledSummary.data.nearestValidFrom
|
||||
? t('dict.scheduled.hint', {
|
||||
count: scheduledSummary.data.count,
|
||||
date: new Date(scheduledSummary.data.nearestValidFrom).toLocaleDateString(),
|
||||
})
|
||||
: undefined
|
||||
}
|
||||
action={
|
||||
scheduledSummary.data && scheduledSummary.data.count > 0 && scheduledSummary.data.nearestValidFrom
|
||||
? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTimeTravelAt(scheduledSummary.data!.nearestValidFrom!)}
|
||||
className="text-cell text-accent hover:underline"
|
||||
>
|
||||
{t('dict.scheduled.cta')}
|
||||
</button>
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{recordsResult.data && recordsResult.data.length > 0 && (
|
||||
|
||||
Reference in New Issue
Block a user