fix(dict): default-hide для existing users (v=2 storage) + sticky header/pagination

This commit is contained in:
Александр Зимин
2026-06-02 12:22:19 +00:00
parent f7fcb316bd
commit fece673017
2 changed files with 64 additions and 28 deletions
+45 -18
View File
@@ -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 не сработает.
*
* <p>v1 (MR !282): юзер сам прятал колонки, default hidden=[].
* <p>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<DictTableState> | 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 его не должен трогать.
*
* <p>После любого 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<DictTableState>
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. */