1// @ts-check 2/** 3 * Saved table-column layouts (M2) â pure list/serialization logic for named column sets. 4 * A layout bundles the visible result-table column keys under a user-given name. The UI 5 * layer (search.js) owns persistence (localStorage) and capturing/applying the live column 6 * selection; this module is framework-free and fully testable. Mirrors saved-filters.js. 7 * 8 * @typedef {Object} SavedLayout 9 * @property {string} id Stable id derived from the name (re-saving a name overwrites). 10 * @property {string} name 11 * @property {string[]} columns Visible column keys, in canonical order. 12 * @property {number} createdAt 13 * @property {number} updatedAt 14 */ 15 16/** localStorage key for the saved-layout list. */ 17export const SAVED_LAYOUTS_KEY = 'mtg-studio-saved-layouts'; 18 19/** 20 * Derive a stable id from a name so re-saving the same name updates in place. 21 * @param {string} name 22 * @returns {string} 23 */ 24export function layoutId(name) { 25 return String(name || '') 26 .trim() 27 .toLowerCase() 28 .replace(/\s+/g, '-') 29 .replace(/[^a-z0-9-]/g, ''); 30} 31 32/** 33 * Add or update a named layout, returning a new list sorted by name. Re-using an existing 34 * name overwrites that layout (preserving its createdAt). 35 * @param {SavedLayout[]} list 36 * @param {string} name 37 * @param {string[]} columns 38 * @param {number} [now] timestamp (injected for determinism in tests) 39 * @returns {SavedLayout[]} 40 */ 41export function upsertLayout(list, name, columns, now = 0) { 42 const trimmed = String(name || '').trim(); 43 if (!trimmed) throw new Error('A layout name is required.'); 44 const cols = Array.isArray(columns) ? columns.filter((c) => typeof c === 'string') : []; 45 if (!cols.length) throw new Error('A layout needs at least one column.'); 46 const id = layoutId(trimmed) || `layout-${now}`; 47 const current = Array.isArray(list) ? list : []; 48 const existing = current.find((l) => l.id === id); 49 const entry = { 50 id, 51 name: trimmed, 52 columns: cols, 53 createdAt: existing ? existing.createdAt : now, 54 updatedAt: now, 55 }; 56 return [...current.filter((l) => l.id !== id), entry].sort((a, b) => 57 a.name.localeCompare(b.name) 58 ); 59} 60 61/** 62 * @param {SavedLayout[]} list 63 * @param {string} id 64 * @returns {SavedLayout[]} 65 */ 66export function removeLayout(list, id) { 67 return (Array.isArray(list) ? list : []).filter((l) => l.id !== id); 68} 69 70/** 71 * @param {SavedLayout[]} list 72 * @param {string} id 73 * @returns {SavedLayout | undefined} 74 */ 75export function findLayout(list, id) { 76 return (Array.isArray(list) ? list : []).find((l) => l.id === id); 77} 78 79/** 80 * Defensively parse a stored JSON string into a validated SavedLayout[]. 81 * Tolerates malformed/legacy data by dropping invalid entries. 82 * @param {string | null | undefined} json 83 * @returns {SavedLayout[]} 84 */ 85export function parseLayouts(json) { 86 if (!json) return []; 87 let arr; 88 try { 89 arr = JSON.parse(json); 90 } catch { 91 return []; 92 } 93 if (!Array.isArray(arr)) return []; 94 return arr.filter( 95 (l) => 96 l && 97 typeof l.id === 'string' && 98 typeof l.name === 'string' && 99 Array.isArray(l.columns) && 100 l.columns.every((/** @type {unknown} */ c) => typeof c === 'string') 101 ); 102} 103 104/** 105 * @param {SavedLayout[]} list 106 * @returns {string} 107 */ 108export function serializeLayouts(list) { 109 return JSON.stringify(Array.isArray(list) ? list : []); 110}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.