import { useCallback, useContext } from 'react' import { ApiServerContext } from '../context/ApiServerContext' export const mergeOrder = (saved = [], defaults = []) => { const defaultSet = new Set(defaults) const kept = saved.filter((key) => defaultSet.has(key)) const keptSet = new Set(kept) const appended = defaults.filter((key) => !keptSet.has(key)) return [...kept, ...appended] } export const COLUMN_PROPORTION_STEP = 5 const MIN_COLUMN_PROPORTION = 5 export const normalizeColumnProportions = (count, saved) => { if (count <= 0) return [] if (count === 1) return [100] if (Array.isArray(saved) && saved.length === count) { return saved.map((value) => { const numeric = Number(value) if (!Number.isFinite(numeric)) return MIN_COLUMN_PROPORTION return Math.max( MIN_COLUMN_PROPORTION, Math.round(numeric / COLUMN_PROPORTION_STEP) * COLUMN_PROPORTION_STEP ) }) } const base = Math.floor(100 / count / COLUMN_PROPORTION_STEP) * COLUMN_PROPORTION_STEP const result = Array(count).fill(Math.max(MIN_COLUMN_PROPORTION, base)) let remainder = 100 - result.reduce((sum, value) => sum + value, 0) let i = 0 while (remainder >= COLUMN_PROPORTION_STEP) { result[i % count] += COLUMN_PROPORTION_STEP remainder -= COLUMN_PROPORTION_STEP i += 1 } return result } const asObject = (value) => value && typeof value === 'object' && !Array.isArray(value) ? value : {} const asStringArray = (value) => Array.isArray(value) ? value.filter((key) => typeof key === 'string') : [] const collectKnownLeaves = (defaultOrder, defaultFlexColumns) => { const leaves = new Set() for (const key of defaultOrder) { if (defaultFlexColumns[key]) { for (const child of defaultFlexColumns[key]) { leaves.add(child) } } else { leaves.add(key) } } return leaves } export const mergeLayout = (saved, defaultLayout) => { const defaultOrder = asStringArray(defaultLayout?.sectionOrder) const defaultFlexColumns = Object.fromEntries( Object.entries(asObject(defaultLayout?.flexColumns)).map(([id, items]) => [ id, asStringArray(items) ]) ) const savedOrder = asStringArray(saved?.sectionOrder) const savedFlexColumns = Object.fromEntries( Object.entries(asObject(saved?.flexColumns)).map(([id, items]) => [ id, asStringArray(items) ]) ) const savedStats = saved?.statsOrder && typeof saved.statsOrder === 'object' && !Array.isArray(saved.statsOrder) ? saved.statsOrder : {} const savedVisibility = saved?.statsVisibility && typeof saved.statsVisibility === 'object' && !Array.isArray(saved.statsVisibility) ? saved.statsVisibility : {} const knownLeaves = collectKnownLeaves(defaultOrder, defaultFlexColumns) const defaultGroupIds = new Set(Object.keys(defaultFlexColumns)) const userGroupIds = Object.keys(savedFlexColumns).filter( (id) => !defaultGroupIds.has(id) && !knownLeaves.has(id) ) const userGroupIdSet = new Set(userGroupIds) const isGroupId = (key) => defaultGroupIds.has(key) || userGroupIdSet.has(key) const placedLeaves = new Set() const takeLeaves = (keys) => { const result = [] for (const key of keys || []) { if (knownLeaves.has(key) && !placedLeaves.has(key)) { placedLeaves.add(key) result.push(key) } } return result } const sectionOrder = [] const flexColumns = {} const addGroup = (id, childKeys) => { flexColumns[id] = childKeys if (!sectionOrder.includes(id)) { sectionOrder.push(id) } } const hasSavedStructure = savedOrder.length > 0 || Object.keys(savedFlexColumns).length > 0 if (hasSavedStructure) { for (const key of savedOrder) { if (knownLeaves.has(key) && !placedLeaves.has(key)) { placedLeaves.add(key) sectionOrder.push(key) } else if (isGroupId(key)) { addGroup( key, takeLeaves(savedFlexColumns[key] ?? defaultFlexColumns[key] ?? []) ) } } } for (const key of defaultOrder) { if (knownLeaves.has(key) && !placedLeaves.has(key)) { placedLeaves.add(key) sectionOrder.push(key) } else if (defaultGroupIds.has(key)) { const remaining = takeLeaves( mergeOrder(savedFlexColumns[key] || [], defaultFlexColumns[key]) ) if (sectionOrder.includes(key)) { flexColumns[key] = [...(flexColumns[key] || []), ...remaining] } else if (remaining.length > 0 || !hasSavedStructure) { addGroup(key, remaining) } } } for (const id of userGroupIds) { if (!sectionOrder.includes(id)) { addGroup(id, takeLeaves(savedFlexColumns[id])) } } for (const leaf of knownLeaves) { if (!placedLeaves.has(leaf)) { placedLeaves.add(leaf) sectionOrder.push(leaf) } } const flexColumnProportions = Object.fromEntries( Object.entries(asObject(saved?.flexColumnProportions)).map( ([id, value]) => [ id, Array.isArray(value) ? value.filter( (item) => typeof item === 'number' && Number.isFinite(item) ) : [] ] ) ) return { sectionOrder, flexColumns, statsOrder: savedStats, statsVisibility: savedVisibility, sectionHeights: asObject(saved?.sectionHeights), flexColumnProportions } } const usePageLayout = ( pageName, defaultLayout = { sectionOrder: [], flexColumns: {}, statsOrder: {}, statsVisibility: {} } ) => { const { userSettings, updateUserSettings } = useContext(ApiServerContext) const saved = userSettings.pageLayout?.[pageName] const layout = mergeLayout(saved, defaultLayout) const updatePageLayout = useCallback( (value) => { updateUserSettings('pageLayout', pageName, value) }, [pageName, updateUserSettings] ) return [layout, updatePageLayout] } export default usePageLayout