PageSourceSearch

https://mtg.studio/js/domain/saved-layouts.js

js mtg.studio collected 2026-10-09 22:17:15 UTC 3,492 bytes, 110 lines download raw bytes

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.