fix(admin-ui): v1.1.1 patch — version routing + guest mode + timeline UX

This commit is contained in:
Александр Зимин
2026-05-10 19:34:47 +00:00
parent 54817e1a9b
commit 69f1a05a1f
8 changed files with 419 additions and 144 deletions
@@ -31,6 +31,7 @@ import {
type RecordsFilter,
} from '@/api/queries'
import { useBulkCloseRecords, useBulkExportRecords, useCreateRecord, useUpdateRecord, useCloseRecord, useFormIdempotencyKey } from '@/api/mutations'
import { useCanMutate } from '@/auth/useCanMutate'
import type { BulkCloseResponse, CreateRecordRequest, DataScope, FlattenedRecord } from '@/api/client'
import { SchemaDrivenForm } from '@/components/form/SchemaDrivenForm'
import { DictionaryEditorDialog } from '@/components/schema/DictionaryEditorDialog'
@@ -39,7 +40,8 @@ import { DictionaryHubView } from '@/components/lineage/DictionaryHubView'
import { CascadeConfirmDialog } from '@/components/lineage/CascadeConfirmDialog'
import { RecordHistoryDrawer } from '@/components/record/RecordHistoryDrawer'
import { AoiPickerDialog, type AoiResult } from '@/components/geo/AoiPickerDialog'
import { nowIsoLocal, toDateTimeLocalInput, fromDateTimeLocalInput } from '@/lib/dates'
import { TimeTravelPicker } from '@/components/timetravel/TimeTravelPicker'
import { nowIsoLocal } from '@/lib/dates'
import { recordDisplayName } from '@/lib/locales'
import { SCOPE_BORDER_TOP, SCOPE_DOT, SCOPE_ORDER } from '@/lib/scope-style'
@@ -103,6 +105,7 @@ function DictionaryDetail() {
const navigate = useNavigate({ from: Route.fullPath })
const { t } = useTranslation()
const detailQuery = useDictionaryDetail(name)
const canMutate = useCanMutate()
// AOI: bbox из URL → bbox-AOI rendered initially; polygon AOI живёт
// только в local state (не сериализуем в URL — слишком длинно).
@@ -523,24 +526,28 @@ function DictionaryDetail() {
>
{t('timeTravel.button')}
</Button>
<Button
variant="secondary"
leftIcon={<GearIcon weight="bold" size={16} />}
disabled={!detailQuery.data}
onClick={() => setSchemaEditOpen(true)}
className="whitespace-nowrap"
>
{t('schema.action.editSchema')}
</Button>
<Button
variant="primary"
leftIcon={<PlusIcon weight="bold" size={16} />}
disabled={!detailQuery.data}
onClick={() => setEdit({ kind: 'create' })}
className="whitespace-nowrap"
>
{t('dict.action.create')}
</Button>
{canMutate && (
<>
<Button
variant="secondary"
leftIcon={<GearIcon weight="bold" size={16} />}
disabled={!detailQuery.data}
onClick={() => setSchemaEditOpen(true)}
className="whitespace-nowrap"
>
{t('schema.action.editSchema')}
</Button>
<Button
variant="primary"
leftIcon={<PlusIcon weight="bold" size={16} />}
disabled={!detailQuery.data}
onClick={() => setEdit({ kind: 'create' })}
className="whitespace-nowrap"
>
{t('dict.action.create')}
</Button>
</>
)}
</div>
}
/>
@@ -612,50 +619,12 @@ function DictionaryDetail() {
ISO datetime в URL ?at=… — share-friendly. Active state показывает
ambient banner справа от input'а. Phase v1 stretch. */}
{timeTravelOpen && (
<div className="flex flex-wrap items-center gap-3 px-3 py-2 rounded-sm border border-orbit/40 bg-orbit/8 text-sm">
<span className="text-xs text-carbon/70 font-secondary uppercase tracking-label">
{t('timeTravel.label')}
</span>
<input
type="datetime-local"
className="px-2 py-1 rounded-sm border border-regolith bg-white text-2xs font-mono"
// datetime-local требует local time без offset. toISOString() отдаёт
// UTC → у пользователя в +03:00 picker показывал значение на 3ч
// раньше выбранного, и на каждом редактировании round-trip drift'ил
// время. toDateTimeLocalInput / fromDateTimeLocalInput сохраняют
// wall-clock время через формат с явным local-TZ offset.
value={toDateTimeLocalInput(timeTravelAt)}
onChange={(e) => {
if (!e.target.value) {
setTimeTravelAt(undefined)
return
}
setTimeTravelAt(fromDateTimeLocalInput(e.target.value))
}}
aria-label={t('timeTravel.label')}
/>
{timeTravelAt && (
<>
<span className="text-2xs text-carbon/70 font-mono">
{new Date(timeTravelAt).toLocaleString()}
</span>
<button
type="button"
className="text-xs text-ultramarain hover:underline"
onClick={() => setTimeTravelAt(undefined)}
>
{t('timeTravel.clear')}
</button>
</>
)}
<button
type="button"
className="text-xs text-carbon/60 hover:underline ml-auto"
onClick={() => setTimeTravelOpen(false)}
>
{t('timeTravel.close')}
</button>
</div>
<TimeTravelPicker
value={timeTravelAt}
onChange={(iso) => setTimeTravelAt(iso)}
onClear={() => setTimeTravelAt(undefined)}
onClose={() => setTimeTravelOpen(false)}
/>
)}
{/* Active state — ambient banner если ?at= задан, даже если picker закрыт. */}
@@ -745,7 +714,7 @@ function DictionaryDetail() {
<EmptyState title={t('dict.filter.noMatches')} />
) : (
<>
{selection.size > 0 && (
{canMutate && selection.size > 0 && (
<BulkSelectionToolbar
count={selection.size}
totalVisible={filteredCount}
@@ -759,14 +728,16 @@ function DictionaryDetail() {
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>
<Checkbox
aria-label={t('dict.bulk.selectAll')}
checked={allVisibleSelected}
ref={selectAllRef}
onChange={toggleSelectAll}
/>
</TableHeaderCell>
{canMutate && (
<TableHeaderCell>
<Checkbox
aria-label={t('dict.bulk.selectAll')}
checked={allVisibleSelected}
ref={selectAllRef}
onChange={toggleSelectAll}
/>
</TableHeaderCell>
)}
<TableHeaderCell>{t('dict.col.businessKey')}</TableHeaderCell>
<TableHeaderCell>name / code</TableHeaderCell>
{extraColumns.map((c) => (
@@ -780,13 +751,15 @@ function DictionaryDetail() {
<TableBody>
{visibleRecords.map((r) => (
<TableRow key={r.id} data-selected={selection.has(r.businessKey)}>
<TableCell>
<Checkbox
aria-label={`${t('dict.bulk.selectRow')} ${r.businessKey}`}
checked={selection.has(r.businessKey)}
onChange={() => toggleSelect(r.businessKey)}
/>
</TableCell>
{canMutate && (
<TableCell>
<Checkbox
aria-label={`${t('dict.bulk.selectRow')} ${r.businessKey}`}
checked={selection.has(r.businessKey)}
onChange={() => toggleSelect(r.businessKey)}
/>
</TableCell>
)}
<TableCell>
<div className="flex items-center gap-1.5">
<span className="font-mono text-2xs">{r.businessKey}</span>
@@ -844,18 +817,22 @@ function DictionaryDetail() {
icon={<ClockCounterClockwiseIcon weight="regular" />}
onClick={() => setHistoryKey(r.businessKey)}
/>
<IconButton
label={t('dict.action.edit')}
variant="default"
icon={<PencilSimpleIcon weight="regular" />}
onClick={() => setEdit({ kind: 'edit', record: r })}
/>
<IconButton
label={t('dict.action.close')}
variant="danger"
icon={<XCircleIcon weight="regular" />}
onClick={() => setEdit({ kind: 'close-confirm', record: r })}
/>
{canMutate && (
<>
<IconButton
label={t('dict.action.edit')}
variant="default"
icon={<PencilSimpleIcon weight="regular" />}
onClick={() => setEdit({ kind: 'edit', record: r })}
/>
<IconButton
label={t('dict.action.close')}
variant="danger"
icon={<XCircleIcon weight="regular" />}
onClick={() => setEdit({ kind: 'close-confirm', record: r })}
/>
</>
)}
</div>
</TableCell>
</TableRow>