import { useCallback, useEffect, useState } from 'react' /** * Per-dict table state — column sort + visibility, persisted в localStorage. * *
Key format: {@code ord-dict-table: Sort: cycle {@code none → asc → desc → none} по той же колонке.
* Переключение колонки начинает с {@code asc}. Reduce friction для
* "посмотреть кто свежее / алфавит / scope grouping".
*
* Hidden: array column-key'ев которые юзер скрыл. {@code businessKey}
* forced visible на уровне UI (см. SortableHeaderCell — primary identifier,
* без него таблица бессмысленна). Прочие — optional.
*/
export type SortDir = 'asc' | 'desc'
export type SortState = { col: string; dir: SortDir } | null
export interface DictTableState {
sort: SortState
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 После любого toggle/setHidden writeState запишет state с current _v →
* дальнейшие посещения respect юзерский выбор вне зависимости от fallback.
*/
const readState = (dictName: string, fallbackHidden: string[]): DictTableState => {
if (typeof window === 'undefined') return { sort: null, hidden: fallbackHidden }
try {
const raw = window.localStorage.getItem(storageKey(dictName))
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: parseSort(parsed),
hidden: Array.isArray(parsed.hidden)
? parsed.hidden.filter((s): s is string => typeof s === 'string')
: [],
}
} catch {
return { sort: null, hidden: fallbackHidden }
}
}
const writeState = (dictName: string, state: DictTableState): void => {
if (typeof window === 'undefined') return
try {
const stored: StoredState = { ...state, _v: STATE_VERSION }
window.localStorage.setItem(storageKey(dictName), JSON.stringify(stored))
} catch {
/* QuotaExceeded / private mode — silently ignore, state остаётся
только в memory. */
}
}
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