diff --git a/ordinis-admin-ui/src/lib/useDictTableState.ts b/ordinis-admin-ui/src/lib/useDictTableState.ts
index 8a07d78..91b7af7 100644
--- a/ordinis-admin-ui/src/lib/useDictTableState.ts
+++ b/ordinis-admin-ui/src/lib/useDictTableState.ts
@@ -24,45 +24,72 @@ export interface DictTableState {
hidden: string[]
}
+/**
+ * Schema version stored в localStorage. Bumpит когда меняется default
+ * visibility logic — старые записи с другим (или missing) _v читаются как
+ * unmigrated, и для них применяется текущий fallback. После первого
+ * write entry обновляется с current version → больше fallback не сработает.
+ *
+ *
v1 (MR !282): юзер сам прятал колонки, default hidden=[].
+ *
v2 (MR after !287): info-dense schemas (10+ scalar полей) hide tail
+ * by default. Юзеры из v1 имеют `{hidden:[]}` в storage — без version
+ * bump мы бы их default-показ оставили (т.е. fix не сработал бы для них).
+ */
+const STATE_VERSION = 2
+
+interface StoredState extends DictTableState {
+ _v?: number
+}
+
const DEFAULT_STATE: DictTableState = { sort: null, hidden: [] }
const storageKey = (dictName: string): string => `ord-dict-table:${dictName}`
+const parseSort = (raw: Partial | null | undefined): SortState => {
+ const s = raw?.sort
+ return s &&
+ typeof s.col === 'string' &&
+ (s.dir === 'asc' || s.dir === 'desc')
+ ? { col: s.col, dir: s.dir }
+ : null
+}
+
/**
- * Прочитать persisted state. Если в storage пусто (юзер ни разу не
- * взаимодействовал с этим dict) → используем `fallbackHidden` как initial
- * visibility (для info-dense schemas: hide tail columns по умолчанию,
- * чтобы таблица помещалась в viewport без H-scroll). Любой toggle/setHidden
- * запишет state в storage → дальнейшие посещения respect юзерский выбор
- * вне зависимости от fallback.
+ * Прочитать persisted state. Если в storage пусто, или запись unversioned/
+ * outdated → возвращаем `fallbackHidden` (для info-dense schemas: hide tail
+ * columns по умолчанию). Sort всё равно preserve'ем — он юзерский выбор,
+ * version bump его не должен трогать.
+ *
+ * После любого toggle/setHidden writeState запишет state с current _v →
+ * дальнейшие посещения respect юзерский выбор вне зависимости от fallback.
*/
const readState = (dictName: string, fallbackHidden: string[]): DictTableState => {
- const fallback: DictTableState = { sort: null, hidden: fallbackHidden }
- if (typeof window === 'undefined') return fallback
+ if (typeof window === 'undefined') return { sort: null, hidden: fallbackHidden }
try {
const raw = window.localStorage.getItem(storageKey(dictName))
- if (!raw) return fallback
- const parsed = JSON.parse(raw) as Partial
+ if (!raw) return { sort: null, hidden: fallbackHidden }
+ const parsed = JSON.parse(raw) as StoredState
+ // Unversioned (v1 → юзер из MR !282) или mismatched _v → reapply
+ // fallback. Sort preserve.
+ if (parsed._v !== STATE_VERSION) {
+ return { sort: parseSort(parsed), hidden: fallbackHidden }
+ }
return {
- sort:
- parsed.sort &&
- typeof parsed.sort.col === 'string' &&
- (parsed.sort.dir === 'asc' || parsed.sort.dir === 'desc')
- ? { col: parsed.sort.col, dir: parsed.sort.dir }
- : null,
+ sort: parseSort(parsed),
hidden: Array.isArray(parsed.hidden)
? parsed.hidden.filter((s): s is string => typeof s === 'string')
: [],
}
} catch {
- return fallback
+ return { sort: null, hidden: fallbackHidden }
}
}
const writeState = (dictName: string, state: DictTableState): void => {
if (typeof window === 'undefined') return
try {
- window.localStorage.setItem(storageKey(dictName), JSON.stringify(state))
+ const stored: StoredState = { ...state, _v: STATE_VERSION }
+ window.localStorage.setItem(storageKey(dictName), JSON.stringify(stored))
} catch {
/* QuotaExceeded / private mode — silently ignore, state остаётся
только в memory. */
diff --git a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx
index efe3759..ff47d1f 100644
--- a/ordinis-admin-ui/src/routes/dictionaries.$name.tsx
+++ b/ordinis-admin-ui/src/routes/dictionaries.$name.tsx
@@ -1209,9 +1209,12 @@ function DictionaryDetail() {
/>
)}
{/* No wrap — outer editor panel handles border. Direct
- * table → continuous visual flow toolbar→table в one card. */}
+ * table → continuous visual flow toolbar→table в one card.
+ * TableHead sticky top: при page V-scroll header не уезжает,
+ * sort/aria всегда видны. z-10 чтобы над cells. bg-surface
+ * чтобы строки под header не просвечивали при scroll. */}
-
+
{canMutate && (
@@ -1422,14 +1425,20 @@ function DictionaryDetail() {
{totalPages > 1 && (
- setPage((p) => Math.max(0, p - 1))}
- onNext={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
- />
+ // Sticky bottom: pagination всегда в viewport при V-scroll
+ // длинной таблицы — не надо листать вниз чтобы перейти на
+ // page 2. bg-surface + border-top чтобы визуально отделить
+ // от scrolling tbody.
+
+ setPage((p) => Math.max(0, p - 1))}
+ onNext={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
+ />
+
)}
>
)}