feat(editor): ExportModal + TimeTravelModal + InfoPanel merge + fixes
UI redesign matching prototype (Ordinis (3) dump): - ExportModal: format (CSV/JSON/Excel/SQL) + scope (all/filtered/selected) + column toggles + encoding/delimiter + preview filename per redesign line 1059-1092. CSV uses backend mutation, JSON client-side from filtered rows, Excel/SQL disabled (backend pending). - TimeTravelModal: full-page compare СЕЙЧАС vs ТОЧКА ВО ВРЕМЕНИ + quick presets (сейчас/неделя/месяц/год/при создании) + step toggle версия/день + slider + tabs (Что изменилось / Записи на момент / Структура и поля) + "Открыть как readonly" CTA. Replaces inline TimeTravelPicker. - InfoPanel ← используют merge: rich rows с field path + active records count + onClose policy chip (BLOCK/WARN/CASCADE). Inline DictionaryDependentsPanel above records table removed (one source of truth). - Sidebar logo: match redesign/ui-kit.html .brand SVG (outer ring + inner faded ring + orbit dot offset right cx=22.5). ORDINIS Tektur tracking 0.22em + MDM smaller mute 0.32em. - Bg neutralized: #fbf8ee Earth cream → #faf9f5 warm neutral default (per user preference, Earth cream still described in comment как opt-in). - WorkflowBanner: Case 1 Live (approvalRequired=false) returns null — permanent "Опубликовано" banner = noise, status already visible в InfoPanel scope/version/updatedAt. Banner показывается только когда есть actionable state. Critical bug fixes: - fix(CascadeConfirmDialog): infinite render loop "Maximum update depth exceeded". useEffect had cascadeMut (TanStack Query mutation wrapper) в deps — wrapper identity changes every render → effect fires → reset() → store update → render → loop. Extract stable `reset` reference перед useEffect. Симптомы у user: создание/редактирование записи не работали. - fix(semantic-release): add missing peer dep conventional-changelog-conventionalcommits (9.3.1). semantic-release 22+ больше не bundles preset. Без неё release job падал с "Cannot find module 'conventional-changelog-conventionalcommits'".
This commit is contained in:
@@ -35,15 +35,15 @@ 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'
|
||||
import { DictionaryDependentsPanel } from '@/components/lineage/DictionaryDependentsPanel'
|
||||
import { DictionaryHubView } from '@/components/lineage/DictionaryHubView'
|
||||
import { CascadeConfirmDialog } from '@/components/lineage/CascadeConfirmDialog'
|
||||
import { RecordHistoryDrawer } from '@/components/record/RecordHistoryDrawer'
|
||||
import { RecordDrawer } from '@/components/record/RecordDrawer'
|
||||
import { WorkflowBanner } from '@/components/editor/WorkflowBanner'
|
||||
import { EditorInfoPanel } from '@/components/editor/EditorInfoPanel'
|
||||
import { ExportModal } from '@/components/editor/ExportModal'
|
||||
import { AoiPickerDialog, type AoiResult } from '@/components/geo/AoiPickerDialog'
|
||||
import { TimeTravelPicker } from '@/components/timetravel/TimeTravelPicker'
|
||||
import { TimeTravelModal } from '@/components/timetravel/TimeTravelModal'
|
||||
import { nowIsoLocal } from '@/lib/dates'
|
||||
import { recordDisplayName } from '@/lib/locales'
|
||||
import { SCOPE_BORDER_TOP, SCOPE_DOT, SCOPE_ORDER } from '@/lib/scope-style'
|
||||
@@ -188,6 +188,7 @@ function DictionaryDetail() {
|
||||
const [historyKey, setHistoryKey] = useState<string | undefined>(undefined)
|
||||
const [aoiOpen, setAoiOpen] = useState(false)
|
||||
const [timeTravelOpen, setTimeTravelOpen] = useState(false)
|
||||
const [exportOpen, setExportOpen] = useState(false)
|
||||
/** Phase 3 dict-relationships-v2: cascade dialog state. Открывается из 409
|
||||
* на close, или явным action из record menu. */
|
||||
const [cascadeKey, setCascadeKey] = useState<string | undefined>(undefined)
|
||||
@@ -570,16 +571,9 @@ function DictionaryDetail() {
|
||||
timeTravelActive: Boolean(timeTravelAt),
|
||||
onAoi: () => setAoiOpen(true),
|
||||
aoiActive: Boolean(aoi),
|
||||
onExport: () => {
|
||||
// Export all visible records (filtered + paginated set OR full
|
||||
// dictionary). Если selection непустой — exports selection,
|
||||
// иначе — все filtered records.
|
||||
const keys =
|
||||
selection.size > 0
|
||||
? Array.from(selection)
|
||||
: filteredRecords.map((r) => r.businessKey)
|
||||
if (keys.length > 0) bulkExportMut.mutate(keys)
|
||||
},
|
||||
// Открываем ExportModal вместо immediate triggera — пользователь
|
||||
// выбирает формат/scope/columns/encoding/delimiter.
|
||||
onExport: () => setExportOpen(true),
|
||||
exportPending: bulkExportMut.isPending,
|
||||
}}
|
||||
/>
|
||||
@@ -664,11 +658,9 @@ function DictionaryDetail() {
|
||||
<HistoryTabContent />
|
||||
)}
|
||||
|
||||
{/* DictionaryDependentsPanel — отображается ТОЛЬКО на Records tab.
|
||||
Phase 1 dict-relationships-v2 reverse FK card. Hide-on-empty. */}
|
||||
{(!urlSearch.tab || urlSearch.tab === 'records') && (
|
||||
<DictionaryDependentsPanel dictionaryName={name} />
|
||||
)}
|
||||
{/* DictionaryDependentsPanel inline-card удалён — reverse FK rows
|
||||
* теперь живут в EditorInfoPanel ← используют (rich rows с field path
|
||||
* + active count + onClose policy chip). Один source of truth. */}
|
||||
|
||||
{aoi && (
|
||||
<div className="flex items-center gap-3 px-3 py-2 rounded-sm border border-accent/30 bg-accent/4 text-body">
|
||||
@@ -693,16 +685,22 @@ function DictionaryDetail() {
|
||||
{(!urlSearch.tab || urlSearch.tab === 'records') && (
|
||||
<>
|
||||
|
||||
{/* Time-travel picker — раскрывается по клику на toolbar button.
|
||||
ISO datetime в URL ?at=… — share-friendly. Active state показывает
|
||||
ambient banner справа от input'а. Phase v1 stretch. */}
|
||||
{timeTravelOpen && (
|
||||
<TimeTravelPicker
|
||||
value={timeTravelAt}
|
||||
onChange={(iso) => setTimeTravelAt(iso)}
|
||||
onClear={() => setTimeTravelAt(undefined)}
|
||||
{/* Time-travel modal — only mount когда open=true. Modal внутри
|
||||
использует Radix Dialog который portals content при open. Но props
|
||||
like `marks={recordTimestamps}` пересоздаются на каждый render
|
||||
родителя (new array reference) — это нормально для уже mounted
|
||||
компонента, но при always-mounted причиняет нестабильность store.
|
||||
Pattern: lazy mount только если open. */}
|
||||
{timeTravelOpen && detailQuery.data && (
|
||||
<TimeTravelModal
|
||||
open
|
||||
onClose={() => setTimeTravelOpen(false)}
|
||||
detail={detailQuery.data}
|
||||
value={timeTravelAt}
|
||||
onApply={(iso) => setTimeTravelAt(iso)}
|
||||
onClear={() => setTimeTravelAt(undefined)}
|
||||
marks={recordTimestamps}
|
||||
totalRecords={totalRecords}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1125,6 +1123,20 @@ function DictionaryDetail() {
|
||||
initial={aoi?.geojson ?? null}
|
||||
/>
|
||||
|
||||
{/* Export modal — only mount when open. Same lazy pattern as Time-travel. */}
|
||||
{exportOpen && detailQuery.data && (
|
||||
<ExportModal
|
||||
open
|
||||
onClose={() => setExportOpen(false)}
|
||||
detail={detailQuery.data}
|
||||
totalCount={totalRecords}
|
||||
filteredRecords={filteredRecords}
|
||||
selection={selection}
|
||||
onExportCsv={(keys) => bulkExportMut.mutate(keys)}
|
||||
exportPending={bulkExportMut.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
isOpen={bulkOpen}
|
||||
onClose={() => {
|
||||
|
||||
Reference in New Issue
Block a user