From b6190713e4e472b0aa1a9f26592b537920b8df8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BB=D0=B5=D0=BA=D1=81=D0=B0=D0=BD=D0=B4=D1=80=20?= =?UTF-8?q?=D0=97=D0=B8=D0=BC=D0=B8=D0=BD?= Date: Tue, 2 Jun 2026 12:14:00 +0000 Subject: [PATCH] =?UTF-8?q?feat(dict):=20=D1=82=D0=B0=D0=B1=D0=BB=D0=B8?= =?UTF-8?q?=D1=86=D0=B0=20=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=BE=D1=87=D0=BD?= =?UTF-8?q?=D0=B8=D0=BA=D0=B0=20=E2=80=94=20hide=20tail=20columns=20=D0=BF?= =?UTF-8?q?=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=D0=BD=D0=B8=D1=8E?= =?UTF-8?q?=20(=D0=B1=D0=B5=D0=B7=20H-scroll)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ordinis-admin-ui/src/lib/useDictTableState.ts | 33 ++++++++++++++----- .../src/routes/dictionaries.$name.tsx | 21 +++++++++++- 2 files changed, 44 insertions(+), 10 deletions(-) diff --git a/ordinis-admin-ui/src/lib/useDictTableState.ts b/ordinis-admin-ui/src/lib/useDictTableState.ts index 5b5d2ac..8a07d78 100644 --- a/ordinis-admin-ui/src/lib/useDictTableState.ts +++ b/ordinis-admin-ui/src/lib/useDictTableState.ts @@ -28,11 +28,20 @@ const DEFAULT_STATE: DictTableState = { sort: null, hidden: [] } const storageKey = (dictName: string): string => `ord-dict-table:${dictName}` -const readState = (dictName: string): DictTableState => { - if (typeof window === 'undefined') return DEFAULT_STATE +/** + * Прочитать persisted state. Если в storage пусто (юзер ни разу не + * взаимодействовал с этим dict) → используем `fallbackHidden` как initial + * visibility (для info-dense schemas: hide tail columns по умолчанию, + * чтобы таблица помещалась в viewport без H-scroll). Любой toggle/setHidden + * запишет state в storage → дальнейшие посещения respect юзерский выбор + * вне зависимости от fallback. + */ +const readState = (dictName: string, fallbackHidden: string[]): DictTableState => { + const fallback: DictTableState = { sort: null, hidden: fallbackHidden } + if (typeof window === 'undefined') return fallback try { const raw = window.localStorage.getItem(storageKey(dictName)) - if (!raw) return DEFAULT_STATE + if (!raw) return fallback const parsed = JSON.parse(raw) as Partial return { sort: @@ -46,7 +55,7 @@ const readState = (dictName: string): DictTableState => { : [], } } catch { - return DEFAULT_STATE + return fallback } } @@ -60,13 +69,19 @@ const writeState = (dictName: string, state: DictTableState): void => { } } -export function useDictTableState(dictName: string) { - const [state, setState] = useState(() => readState(dictName)) +export function useDictTableState(dictName: string, defaultHidden: string[] = []) { + // Stable key для defaultHidden чтобы useEffect не зацикливался на каждый + // render (массив literal каждый раз new reference). Caller обычно строит + // defaultHidden из useMemo, но guard всё равно нужен. + const defaultKey = defaultHidden.join(',') + const [state, setState] = useState(() => readState(dictName, defaultHidden)) - // Перечитать когда смена dictName (navigation между справочниками). + // Перечитать когда смена dictName (navigation между справочниками) или + // когда меняется набор defaultHidden (schema reload → новые extra columns). useEffect(() => { - setState(readState(dictName)) - }, [dictName]) + setState(readState(dictName, defaultHidden)) + // eslint-disable-next-line react-hooks/exhaustive-deps -- defaultKey stable + }, [dictName, defaultKey]) useEffect(() => { writeState(dictName, state) diff --git a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx index 29a1ea4..efe3759 100644 --- a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx +++ b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx @@ -372,9 +372,28 @@ function DictionaryDetail() { setPage(0) }, [search, scopeFilter, aoi]) + // First-visit defaults: с info-dense schemas (10+ scalar полей) полная + // таблица не помещается в viewport → H-scroll. Hide хвост по умолчанию + // — показываем businessKey, name, scope, validFrom + первые 2 extra + // (обычно самые важные ключи). Юзер открывает menu Колонки и сам + // добавляет нужные — выбор persisted в localStorage и при следующем + // визите fallback уже не применяется. + const defaultHiddenExtras = useMemo(() => { + const props = detailQuery.data?.schemaJson?.properties ?? {} + const eligible: string[] = [] + for (const [key, prop] of Object.entries(props)) { + if (key === 'name' || key === 'code' || key === 'businessKey') continue + if (Boolean(prop['x-localized'])) continue + const t = prop.type + if (t === 'array' || t === 'object') continue + eligible.push(key) + } + return eligible.slice(2) // первые 2 видны, остальные default-hidden + }, [detailQuery.data?.schemaJson]) + // Table state — sort (cycle none→asc→desc) + hidden columns persisted в // localStorage per dict через useDictTableState. - const dictTable = useDictTableState(name) + const dictTable = useDictTableState(name, defaultHiddenExtras) // Getter для applySort — извлекает значение по col key из record. // Изолирован чтобы sort работал унифицировано для всех типов колонок.