fix(records): scheduled marks on time-travel + non-empty banner + editor validFrom prefill
Три связанных UX бага после v2.31.0 release: 1. **TimeTravel marks**: после !220 slider extended до now+30d, но отметок в future window не было → blind drag. Backend теперь возвращает upcomingValidFroms (top 50) в /scheduled-summary; frontend merges как marks. 2. **Non-empty banner**: empty-state hint работает только когда список пуст. Если currently active записи есть AND ещё запланированы — пользователь не видит. Добавлен info banner над таблицей с тем же текстом + CTA. 3. **Editor "Действует с"**: edit mode оставлял поле пустым (bitemporal policy «новая версия с now»). User feedback: «показывает ничего, а даты заданы» — выглядит как баг. Prefill nowIsoLocal() — теперь юзер видит конкретную дату как точку отсчёта.
This commit is contained in:
@@ -770,4 +770,6 @@ export type NotificationEventType = (typeof NOTIFICATION_EVENT_TYPES)[number]
|
||||
export type ScheduledRecordsSummary = {
|
||||
count: number
|
||||
nearestValidFrom: string | null
|
||||
/** Top-50 sorted asc upcoming validFrom ISO timestamps — для TimeTravel marks. */
|
||||
upcomingValidFroms: string[]
|
||||
}
|
||||
|
||||
@@ -295,15 +295,22 @@ function DictionaryDetail() {
|
||||
|
||||
// Уникальные validFrom timestamps для timeline marks. Используем raw
|
||||
// recordsResult (не filtered) чтобы видеть все версии независимо от scope/q
|
||||
// фильтров.
|
||||
// фильтров. PLUS scheduled upcoming validFroms — user feedback: «на таймтревел
|
||||
// отметки нужны где записи есть или меняются, а не просто +30 и ищи потом».
|
||||
// После !220 расширили range до +30 дней, теперь добавляем future record
|
||||
// validFroms как marks чтобы slider показывал «вот тут запись появится».
|
||||
const recordTimestamps = useMemo(() => {
|
||||
const set = new Set<number>()
|
||||
for (const r of recordsResult.data ?? []) {
|
||||
const ts = new Date(r.validFrom).getTime()
|
||||
if (!Number.isNaN(ts)) set.add(ts)
|
||||
}
|
||||
for (const iso of scheduledSummary.data?.upcomingValidFroms ?? []) {
|
||||
const ts = new Date(iso).getTime()
|
||||
if (!Number.isNaN(ts)) set.add(ts)
|
||||
}
|
||||
return Array.from(set).sort((a, b) => a - b)
|
||||
}, [recordsResult.data])
|
||||
}, [recordsResult.data, scheduledSummary.data?.upcomingValidFroms])
|
||||
|
||||
// Column filters — per-column value match (e.g. status='operational',
|
||||
// operator='Роскосмос'). Auto-derived из schema enum/x-references properties.
|
||||
@@ -1011,6 +1018,31 @@ function DictionaryDetail() {
|
||||
/>
|
||||
)}
|
||||
|
||||
{recordsResult.data && recordsResult.data.length > 0
|
||||
&& scheduledSummary.data && scheduledSummary.data.count > 0
|
||||
&& scheduledSummary.data.nearestValidFrom && (
|
||||
// Info banner для non-empty listing когда есть scheduled records.
|
||||
// User feedback: «нету отметки что есть запись в будущем» — table
|
||||
// shows только currently active, юзер не видит upcoming. Banner
|
||||
// duplicates EmptyState hint в non-empty case + CTA jump-to-date.
|
||||
<div className="flex items-center gap-3 px-3 py-2 mb-2 rounded-sm border border-warn bg-warn-bg text-body">
|
||||
<ClockCounterClockwiseIcon weight="bold" size={16} className="text-warn shrink-0" />
|
||||
<span className="text-cell text-ink">
|
||||
{t('dict.scheduled.hint', {
|
||||
count: scheduledSummary.data.count,
|
||||
date: new Date(scheduledSummary.data.nearestValidFrom).toLocaleDateString(),
|
||||
})}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setTimeTravelAt(scheduledSummary.data!.nearestValidFrom!)}
|
||||
className="text-cell text-accent hover:underline ml-auto"
|
||||
>
|
||||
{t('dict.scheduled.cta')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{recordsResult.data && recordsResult.data.length > 0 && (
|
||||
<>
|
||||
{/* Records toolbar — attached к table panel (tinted top section,
|
||||
@@ -1332,11 +1364,14 @@ function DictionaryDetail() {
|
||||
businessKey: rawRecordQuery.data.businessKey,
|
||||
data: rawRecordQuery.data.data,
|
||||
// Edit в bitemporal model = «создаём новую версию с этого момента,
|
||||
// старая закрывается». validFrom оставляем пустым — handleSubmit
|
||||
// подставит nowIsoLocal() at submit-time (review #5: иначе stale
|
||||
// если user долго держит drawer открытым).
|
||||
// старая закрывается». Prefill nowIsoLocal() чтобы юзер видел
|
||||
// конкретную дату («сейчас» как точка отсчёта), а не пустое поле.
|
||||
// User feedback: "Действует с показывает ничего, а даты заданы" —
|
||||
// empty field выглядит как баг. handleSubmit всё равно подставит
|
||||
// свежий nowIsoLocal() если поле не тронуто (review #5 защита от
|
||||
// stale значения если drawer долго открыт).
|
||||
// validTo пустой = бессрочно (или до следующей правки).
|
||||
validFrom: '',
|
||||
validFrom: nowIsoLocal(),
|
||||
validTo: '',
|
||||
}}
|
||||
onSubmit={handleSubmit}
|
||||
|
||||
Reference in New Issue
Block a user