diff --git a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx index 214757d..4508069 100644 --- a/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx +++ b/ordinis-admin-ui/src/components/form/SchemaDrivenForm.tsx @@ -59,6 +59,14 @@ type Props = { * - 'sections': all buckets vertically scrollable + section chips strip * на верху для quick jump. Per handoff Screen 3 RecordDrawer design. */ layout?: 'tabs' | 'sections' + /** Field name search filter (drawer toolbar). Case-insensitive substring match + * против labelOf(key) AND key. Empty = no filter. */ + searchFilter?: string + /** Show only fields с non-empty values (drawer toolbar 'только заполненные'). + * Empty = number 0, '', null, undefined, false, []. */ + onlyFilled?: boolean + /** Notifies parent of visible/total field counters (for drawer toolbar 'N / M'). */ + onCounterChange?: (visible: number, total: number) => void } const ajv = new Ajv({ allErrors: true, strict: false }) @@ -104,6 +112,9 @@ export const SchemaDrivenForm = ({ formId, onDirtyChange, layout = 'tabs', + searchFilter, + onlyFilled, + onCounterChange, }: Props) => { const { t } = useTranslation() const [activeTab, setActiveTab] = useState('identity') @@ -134,6 +145,48 @@ export const SchemaDrivenForm = ({ const idSource = (schema as { 'x-id-source'?: string })['x-id-source'] + // === Drawer toolbar filtering === + // search: substring match (case-insensitive) против key OR labelOf(key) + // onlyFilled: hide fields где data[key] empty (null/undefined/''/false/[]/0) + // Применяется только в sections layout (drawer use case). + const dataValues = (mode === 'edit' ? defaultValues?.data : undefined) ?? {} + const isFieldVisible = useMemo(() => { + const search = (searchFilter ?? '').trim().toLowerCase() + return (key: string): boolean => { + if (search) { + const label = labelOf(key, properties[key]).toLowerCase() + if (!key.toLowerCase().includes(search) && !label.includes(search)) return false + } + if (onlyFilled) { + const v = (dataValues as Record)[key] + if (v == null || v === '' || v === false || v === 0) return false + if (Array.isArray(v) && v.length === 0) return false + if (typeof v === 'object' && Object.keys(v as object).length === 0) return false + } + return true + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [searchFilter, onlyFilled, properties, dataValues, t]) + + // Apply filter ON TOP of bucketing — каждый bucket после filter содержит + // visible subset. Used для render + counters. + const filteredBuckets = useMemo( + () => ({ + identity: buckets.identity.filter(isFieldVisible), + description: buckets.description.filter(isFieldVisible), + extra: buckets.extra.filter(isFieldVisible), + }), + [buckets, isFieldVisible], + ) + + // Counter visible/total для drawer toolbar caption "N / M". + const totalFieldCount = buckets.identity.length + buckets.description.length + buckets.extra.length + const visibleFieldCount = + filteredBuckets.identity.length + filteredBuckets.description.length + filteredBuckets.extra.length + useEffect(() => { + onCounterChange?.(visibleFieldCount, totalFieldCount) + }, [visibleFieldCount, totalFieldCount, onCounterChange]) + const submit: SubmitHandler = (values) => { if (values.validFrom && values.validTo) { const from = new Date(values.validFrom) @@ -225,7 +278,7 @@ export const SchemaDrivenForm = ({ > {tab.label} - {buckets[tab.id as TabId].length} + {filteredBuckets[tab.id as TabId].length} ))} @@ -288,7 +341,7 @@ export const SchemaDrivenForm = ({ /> )} /> - {buckets.identity.map((key) => ( + {filteredBuckets.identity.map((key) => ( - {showAllSections && buckets.description.length > 0 && ( + {showAllSections && filteredBuckets.description.length > 0 && (

{t('form.tabs.description')}

)}
- {buckets.description.map((key) => ( + {filteredBuckets.description.map((key) => ( - {showAllSections && buckets.extra.length > 0 && ( + {showAllSections && filteredBuckets.extra.length > 0 && (

{t('form.tabs.extra')}

)}
- {buckets.extra.map((key) => ( + {filteredBuckets.extra.map((key) => ( ({ + visible: 0, + total: 0, + }) const [schemaEditOpen, setSchemaEditOpen] = useState(false) const [historyKey, setHistoryKey] = useState(undefined) const [aoiOpen, setAoiOpen] = useState(false) @@ -831,12 +838,12 @@ function DictionaryDetail() { )} {t('dict.col.businessKey')} - name / code + name / code {extraColumns.map((c) => ( - {c.label} + {c.label} ))} - {t('dict.col.scope')} - {t('dict.col.validFrom')} + {t('dict.col.scope')} + {t('dict.col.validFrom')} {t('dict.col.actions')} @@ -866,7 +873,7 @@ function DictionaryDetail() { ) : null}
- + {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 ( - + ) } const text = String(v) return ( - + ) })} - + {r.dataScope} - + {new Date(r.validFrom).toLocaleDateString()} @@ -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} /> )}