feat: wire drawer toolbar filters + records table responsive hiding

Closing 2 handoff gaps:

1) DRAWER TOOLBAR FILTERS (handoff Screen 3 line 279)
SchemaDrivenForm получил три new optional props:
  - searchFilter (string) — case-insensitive substring против field key + label
  - onlyFilled (boolean) — hide fields с empty data[key]
    (null/undefined/''/false/0/empty-array/empty-object)
  - onCounterChange (visible, total) — surface field counter parent'у

Filter применяется поверх bucketing — все три buckets (identity/description/
extra) фильтруются. UI рендерит filteredBuckets.X.map(...) вместо raw bucket'ов.

В sections layout (drawer use case) если section после filter empty —
sticky header не рендерится. Tabs layout backward-compat (filter применяется
но visually invisible если user в другом tab'е).

dictionaries.$name.tsx callsite:
  - State: drawerSearch, drawerOnlyFilled, drawerCount {visible, total}
  - Reset on drawer close
  - RecordDrawer.toolbar prop wired только в edit mode (create — все поля
    empty, 'только заполненные' даст 0/0)
  - SchemaDrivenForm receives все три filter props

Result: user может typing 'mass' в drawer search → видит только mass_kg /
data_mass / etc.; toggle 'только заполненные' → hide unused optional fields.

2) RECORDS TABLE RESPONSIVE COLUMN HIDING (handoff line 466)
Records table в dictionaries.$name.tsx применяет hideBelow API из MR !59:
  - businessKey: всегда visible
  - name/code: hideBelow='md' (768px)
  - extra schema columns: hideBelow='lg' (1024px)
  - scope badge: hideBelow='sm' (640px)
  - validFrom: hideBelow='lg'
  - actions: всегда visible

На mobile 375px только businessKey + actions visible — chrome остаётся
usable. Tablet 768+ adds name/scope. Desktop 1024+ adds schema columns.

Tests: 116 pass.
This commit is contained in:
Zimin A.N.
2026-05-11 17:10:26 +03:00
parent e5572d5346
commit 889207f629
2 changed files with 97 additions and 15 deletions
@@ -178,6 +178,13 @@ function DictionaryDetail() {
const [closeReason, setCloseReason] = useState('')
// Dirty-field count surfaced by SchemaDrivenForm → shown в RecordDrawer footer.
const [recordDirtyCount, setRecordDirtyCount] = useState(0)
// Drawer toolbar state per handoff Screen 3 line 279 (search + onlyFilled + counter).
const [drawerSearch, setDrawerSearch] = useState('')
const [drawerOnlyFilled, setDrawerOnlyFilled] = useState(false)
const [drawerCount, setDrawerCount] = useState<{ visible: number; total: number }>({
visible: 0,
total: 0,
})
const [schemaEditOpen, setSchemaEditOpen] = useState(false)
const [historyKey, setHistoryKey] = useState<string | undefined>(undefined)
const [aoiOpen, setAoiOpen] = useState(false)
@@ -831,12 +838,12 @@ function DictionaryDetail() {
</TableHeaderCell>
)}
<TableHeaderCell>{t('dict.col.businessKey')}</TableHeaderCell>
<TableHeaderCell>name / code</TableHeaderCell>
<TableHeaderCell hideBelow="md">name / code</TableHeaderCell>
{extraColumns.map((c) => (
<TableHeaderCell key={c.key}>{c.label}</TableHeaderCell>
<TableHeaderCell key={c.key} hideBelow="lg">{c.label}</TableHeaderCell>
))}
<TableHeaderCell>{t('dict.col.scope')}</TableHeaderCell>
<TableHeaderCell>{t('dict.col.validFrom')}</TableHeaderCell>
<TableHeaderCell hideBelow="sm">{t('dict.col.scope')}</TableHeaderCell>
<TableHeaderCell hideBelow="lg">{t('dict.col.validFrom')}</TableHeaderCell>
<TableHeaderCell align="right">{t('dict.col.actions')}</TableHeaderCell>
</TableRow>
</TableHead>
@@ -866,7 +873,7 @@ function DictionaryDetail() {
) : null}
</div>
</TableCell>
<TableCell>
<TableCell hideBelow="md">
{recordDisplayName(
r.data,
detailQuery.data?.defaultLocale ?? 'ru-RU',
@@ -876,14 +883,14 @@ function DictionaryDetail() {
const v = r.data?.[c.key]
if (v == null || v === '') {
return (
<TableCell key={c.key}>
<TableCell key={c.key} hideBelow="lg">
<span className="text-cell text-mute/70"></span>
</TableCell>
)
}
const text = String(v)
return (
<TableCell key={c.key}>
<TableCell key={c.key} hideBelow="lg">
<span
className={[
'inline-block px-1.5 py-0.5 rounded-sm font-mono text-cell',
@@ -897,10 +904,10 @@ function DictionaryDetail() {
</TableCell>
)
})}
<TableCell>
<TableCell hideBelow="sm">
<Badge variant="info">{r.dataScope}</Badge>
</TableCell>
<TableCell>
<TableCell hideBelow="lg">
<span className="text-cell text-mute">
{new Date(r.validFrom).toLocaleDateString()}
</span>
@@ -963,6 +970,8 @@ function DictionaryDetail() {
onClose={() => {
setEdit({ kind: 'closed' })
setRecordDirtyCount(0)
setDrawerSearch('')
setDrawerOnlyFilled(false)
}}
caption={
edit.kind === 'edit'
@@ -973,6 +982,20 @@ function DictionaryDetail() {
formId="record-form"
dirtyCount={recordDirtyCount}
isPending={Boolean(activeMutation?.isPending)}
// Toolbar показывается только в edit mode где filters имеют смысл.
// Create mode — все поля пустые, "только заполненные" даст empty list.
toolbar={
edit.kind === 'edit'
? {
search: drawerSearch,
onSearchChange: setDrawerSearch,
onlyFilled: drawerOnlyFilled,
onOnlyFilledChange: setDrawerOnlyFilled,
counterVisible: drawerCount.visible,
counterTotal: drawerCount.total,
}
: undefined
}
onDelete={
edit.kind === 'edit'
? () => setEdit({ kind: 'close-confirm', record: edit.record })
@@ -996,6 +1019,9 @@ function DictionaryDetail() {
onCancel={() => setEdit({ kind: 'closed' })}
formId="record-form"
layout="sections"
searchFilter={drawerSearch}
onlyFilled={drawerOnlyFilled}
onCounterChange={(visible, total) => setDrawerCount({ visible, total })}
onDirtyChange={setRecordDirtyCount}
/>
)}
@@ -1033,6 +1059,9 @@ function DictionaryDetail() {
onCancel={() => setEdit({ kind: 'closed' })}
formId="record-form"
layout="sections"
searchFilter={drawerSearch}
onlyFilled={drawerOnlyFilled}
onCounterChange={(visible, total) => setDrawerCount({ visible, total })}
onDirtyChange={setRecordDirtyCount}
/>
)}