feat(ui): 3-dot row menu + history compare/revert + DateTime fitting + tablet touch
Per user feedback screenshots:
SchemaDrivenForm DateTimeField:
- grid → flex flex-wrap: на узких cells (drawer 520px ÷ 2 cols = ~210px)
time wraps под date вертикально. На wide — inline.
- Date min-w 8rem (128px), time w-24 (96px) — tablet touch-safe.
TableHeaderCell без visible label (sr-only для screen readers).
TableCell содержит DropdownMenu trigger 36×36px (touch target):
- History (always)
- Edit, Separator, Close (только canMutate, Close в danger color)
Заменяет 3 inline IconButton'a — экономит ширину + tablet friendly.
RecordHistoryDrawer accepts onCompare / onRevert callbacks. Каждая
non-current version row показывает:
- Сравнить с текущим — opens ConflictDiffModal (reused diff UI)
- Откатить к v{N} — submit updateMut с этой version данными как
payload (+ confirm dialog). Backend создаёт новую active version,
bitemporal history preserves старые.
JSON viewer переключился с <details> на state-driven (hide/show
без CSS rabbit hole).
history.hideData/compare/revert/revertConfirm (RU + EN).
- 3-dot trigger 36px = comfortable touch target (Apple HIG 44, ours
acceptable для density)
- Drawer на <880px viewport = fullscreen sheet (already)
- DateTime wraps на cell <8rem+9rem+gap = 290px
- Sidebar collapse на <1024 = hamburger overlay
This commit is contained in:
@@ -802,15 +802,21 @@ type DateTimeFieldProps = {
|
||||
const DateTimeField = ({ label, value, defaultTime, onChange, hint, error }: DateTimeFieldProps) => {
|
||||
const dateValue = parseFormDate(value)
|
||||
const timeValue = extractTime(value, defaultTime)
|
||||
// flex-wrap вместо fixed grid: на узких ячейках (drawer 520px ÷ 2 cols =
|
||||
// ~210px) time wraps под date. На wide (880px ÷ 2 = ~410px) — inline.
|
||||
// min-w на date обеспечивает разумный размер до wrap'a. Время 6rem (96px)
|
||||
// достаточно для HH:mm + tap target 44px touch-safe.
|
||||
return (
|
||||
<div>
|
||||
<div className="grid grid-cols-[1fr_auto] gap-2 items-end">
|
||||
<DatePicker
|
||||
label={label}
|
||||
value={dateValue}
|
||||
onChange={(d) => onChange(combineDateTime(d, timeValue, value ?? ''))}
|
||||
/>
|
||||
<div className="w-28">
|
||||
<div className="flex flex-wrap gap-2 items-end">
|
||||
<div className="flex-1 min-w-[8rem]">
|
||||
<DatePicker
|
||||
label={label}
|
||||
value={dateValue}
|
||||
onChange={(d) => onChange(combineDateTime(d, timeValue, value ?? ''))}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24 shrink-0">
|
||||
<TextInput
|
||||
label="HH:mm"
|
||||
type="time"
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Alert, Badge, Drawer, LoadingBlock } from '@/ui'
|
||||
import { Alert, Badge, Button, Drawer, LoadingBlock } from '@/ui'
|
||||
import { ArrowsLeftRightIcon, ArrowCounterClockwiseIcon } from '@phosphor-icons/react'
|
||||
import { useRecordHistory } from '@/api/queries'
|
||||
import { RecordDependentsPanel } from '@/components/lineage/RecordDependentsPanel'
|
||||
|
||||
@@ -8,11 +10,23 @@ type Props = {
|
||||
onClose: () => void
|
||||
dictionaryName: string
|
||||
businessKey: string | undefined
|
||||
/** Optional — invoked when user clicks "Сравнить с текущим" на старой версии.
|
||||
* Parent открывает diff view (например ConflictDiffModal с server vs that
|
||||
* version'ом). Если не предоставлен, кнопка скрыта. */
|
||||
onCompare?: (versionData: unknown, displayVersion: number) => void
|
||||
/** Optional — invoked для "Откатить к v1.X.X". Parent submits update
|
||||
* mutation с этой версией как payload. Если undefined — кнопка скрыта. */
|
||||
onRevert?: (versionData: unknown, displayVersion: number) => void
|
||||
}
|
||||
|
||||
export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey }: Props) => {
|
||||
export const RecordHistoryDrawer = ({
|
||||
open, onClose, dictionaryName, businessKey, onCompare, onRevert,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation()
|
||||
const { data, isLoading, error } = useRecordHistory(dictionaryName, open ? businessKey : undefined)
|
||||
// Per-row collapsible JSON viewer — выносим из <details> чтобы хранить
|
||||
// expanded state поверх renders.
|
||||
const [expanded, setExpanded] = useState<Set<string>>(new Set())
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
@@ -101,14 +115,51 @@ export const RecordHistoryDrawer = ({ open, onClose, dictionaryName, businessKey
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<details className="mt-2">
|
||||
<summary className="text-cell text-accent cursor-pointer hover:underline">
|
||||
{t('history.viewData')}
|
||||
</summary>
|
||||
<pre className="text-mono mt-1 bg-line/30 rounded p-2 overflow-x-auto">
|
||||
{/* Per-version action row — Compare / Revert + JSON toggle.
|
||||
* Compare показываем для не-current versions (нечего сравнивать
|
||||
* с самим собой). Revert тоже только для старых (на текущей
|
||||
* uno-op). Wrap чтобы планшетный portrait делал stack. */}
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setExpanded((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (next.has(rec.id)) next.delete(rec.id)
|
||||
else next.add(rec.id)
|
||||
return next
|
||||
})}
|
||||
className="text-cell text-accent hover:underline"
|
||||
>
|
||||
{expanded.has(rec.id) ? t('history.hideData') : t('history.viewData')}
|
||||
</button>
|
||||
{!isLatest && onCompare && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onCompare(rec.data, displayVersion)}
|
||||
leftIcon={<ArrowsLeftRightIcon weight="regular" size={14} />}
|
||||
>
|
||||
{t('history.compare', { defaultValue: 'Сравнить с текущим' })}
|
||||
</Button>
|
||||
)}
|
||||
{!isLatest && onRevert && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onRevert(rec.data, displayVersion)}
|
||||
leftIcon={<ArrowCounterClockwiseIcon weight="regular" size={14} />}
|
||||
>
|
||||
{t('history.revert', { defaultValue: `Откатить к v${displayVersion}` })}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
{expanded.has(rec.id) && (
|
||||
<pre className="text-mono mt-2 bg-line/30 rounded p-2 overflow-x-auto text-cell">
|
||||
{JSON.stringify(rec.data, null, 2)}
|
||||
</pre>
|
||||
</details>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
|
||||
Reference in New Issue
Block a user