fix(dict): default-hide для existing users (v=2 storage) + sticky header/pagination
This commit is contained in:
@@ -24,45 +24,72 @@ export interface DictTableState {
|
|||||||
hidden: string[]
|
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 DEFAULT_STATE: DictTableState = { sort: null, hidden: [] }
|
||||||
|
|
||||||
const storageKey = (dictName: string): string => `ord-dict-table:${dictName}`
|
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 пусто (юзер ни разу не
|
* Прочитать persisted state. Если в storage пусто, или запись unversioned/
|
||||||
* взаимодействовал с этим dict) → используем `fallbackHidden` как initial
|
* outdated → возвращаем `fallbackHidden` (для info-dense schemas: hide tail
|
||||||
* visibility (для info-dense schemas: hide tail columns по умолчанию,
|
* columns по умолчанию). Sort всё равно preserve'ем — он юзерский выбор,
|
||||||
* чтобы таблица помещалась в viewport без H-scroll). Любой toggle/setHidden
|
* version bump его не должен трогать.
|
||||||
* запишет state в storage → дальнейшие посещения respect юзерский выбор
|
*
|
||||||
* вне зависимости от fallback.
|
* <p>После любого toggle/setHidden writeState запишет state с current _v →
|
||||||
|
* дальнейшие посещения respect юзерский выбор вне зависимости от fallback.
|
||||||
*/
|
*/
|
||||||
const readState = (dictName: string, fallbackHidden: string[]): DictTableState => {
|
const readState = (dictName: string, fallbackHidden: string[]): DictTableState => {
|
||||||
const fallback: DictTableState = { sort: null, hidden: fallbackHidden }
|
if (typeof window === 'undefined') return { sort: null, hidden: fallbackHidden }
|
||||||
if (typeof window === 'undefined') return fallback
|
|
||||||
try {
|
try {
|
||||||
const raw = window.localStorage.getItem(storageKey(dictName))
|
const raw = window.localStorage.getItem(storageKey(dictName))
|
||||||
if (!raw) return fallback
|
if (!raw) return { sort: null, hidden: fallbackHidden }
|
||||||
const parsed = JSON.parse(raw) as Partial<DictTableState>
|
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 {
|
return {
|
||||||
sort:
|
sort: parseSort(parsed),
|
||||||
parsed.sort &&
|
|
||||||
typeof parsed.sort.col === 'string' &&
|
|
||||||
(parsed.sort.dir === 'asc' || parsed.sort.dir === 'desc')
|
|
||||||
? { col: parsed.sort.col, dir: parsed.sort.dir }
|
|
||||||
: null,
|
|
||||||
hidden: Array.isArray(parsed.hidden)
|
hidden: Array.isArray(parsed.hidden)
|
||||||
? parsed.hidden.filter((s): s is string => typeof s === 'string')
|
? parsed.hidden.filter((s): s is string => typeof s === 'string')
|
||||||
: [],
|
: [],
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return fallback
|
return { sort: null, hidden: fallbackHidden }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const writeState = (dictName: string, state: DictTableState): void => {
|
const writeState = (dictName: string, state: DictTableState): void => {
|
||||||
if (typeof window === 'undefined') return
|
if (typeof window === 'undefined') return
|
||||||
try {
|
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 {
|
} catch {
|
||||||
/* QuotaExceeded / private mode — silently ignore, state остаётся
|
/* QuotaExceeded / private mode — silently ignore, state остаётся
|
||||||
только в memory. */
|
только в memory. */
|
||||||
|
|||||||
@@ -1209,9 +1209,12 @@ function DictionaryDetail() {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* No <Panel> wrap — outer editor panel handles border. Direct
|
{/* No <Panel> 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. */}
|
||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead className="sticky top-0 z-10 bg-surface">
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{canMutate && (
|
{canMutate && (
|
||||||
<TableHeaderCell>
|
<TableHeaderCell>
|
||||||
@@ -1422,6 +1425,11 @@ function DictionaryDetail() {
|
|||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
{totalPages > 1 && (
|
{totalPages > 1 && (
|
||||||
|
// Sticky bottom: pagination всегда в viewport при V-scroll
|
||||||
|
// длинной таблицы — не надо листать вниз чтобы перейти на
|
||||||
|
// page 2. bg-surface + border-top чтобы визуально отделить
|
||||||
|
// от scrolling tbody.
|
||||||
|
<div className="sticky bottom-0 z-10 bg-surface border-t border-line">
|
||||||
<RecordsPagination
|
<RecordsPagination
|
||||||
page={page}
|
page={page}
|
||||||
totalPages={totalPages}
|
totalPages={totalPages}
|
||||||
@@ -1430,6 +1438,7 @@ function DictionaryDetail() {
|
|||||||
onPrev={() => setPage((p) => Math.max(0, p - 1))}
|
onPrev={() => setPage((p) => Math.max(0, p - 1))}
|
||||||
onNext={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
onNext={() => setPage((p) => Math.min(totalPages - 1, p + 1))}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user