import { Accessor, createEffect, createMemo, untrack } from "solid-js"; import { createStore } from "solid-js/store"; import { CustomPartial } from "solid-js/store/types/store.js"; import { deepCopy, deepDiff, MutarionKind, Mutation } from "~/utilities"; export type DataSetRowNode = { kind: 'row', key: K, value: T } export type DataSetGroupNode = { kind: 'group', key: K, groupedBy: keyof T, nodes: DataSetNode[] }; export type DataSetNode = DataSetRowNode | DataSetGroupNode; export interface SortingFunction { (a: T, b: T): -1 | 0 | 1; } export interface SortOptions> { by: keyof T; reversed: boolean; with?: SortingFunction; } export interface GroupingFunction { (nodes: DataSetRowNode[]): DataSetNode[]; } export interface GroupOptions> { by: keyof T; with?: GroupingFunction; } interface DataSetState> { value: (T | undefined)[]; snapshot: (T | undefined)[]; sorting?: SortOptions; grouping?: GroupOptions; } export type Setter = | T | CustomPartial | ((prevState: T) => T | CustomPartial); export interface DataSet> { nodes: Accessor[]>; mutations: Accessor; readonly value: (T | undefined)[]; readonly sorting: SortOptions | undefined; readonly grouping: GroupOptions | undefined; mutate(index: number, prop: K, value: T[K]): void; mutateEach(setter: (value: T) => T): void; remove(indices: number[]): void; insert(item: T, at?: number): void; sort(options: Setter | undefined>): DataSet; group(options: Setter | undefined>): DataSet; } const defaultComparer = (a: T, b: T) => a < b ? -1 : a > b ? 1 : 0; function defaultGroupingFunction(groupBy: keyof T): GroupingFunction { return (nodes: DataSetRowNode[]): DataSetNode[] => Object.entries(Object.groupBy(nodes, r => r.value[groupBy] as PropertyKey)) .map(([key, nodes]) => ({ kind: 'group', key, groupedBy: groupBy, nodes: nodes! } as DataSetGroupNode)); } export const createDataSet = >(data: Accessor, initialOptions?: { sort?: SortOptions, group?: GroupOptions }): DataSet => { const [state, setState] = createStore>({ value: deepCopy(data()), snapshot: data(), sorting: initialOptions?.sort, grouping: initialOptions?.group, }); const nodes = createMemo(() => { const sorting = state.sorting; const grouping = state.grouping; let value: DataSetNode[] = state.value .map | undefined>((value, key) => value === undefined ? undefined : ({ kind: 'row', key, value })) .filter(node => node !== undefined); if (sorting) { const comparer = sorting.with ?? defaultComparer; value = value.filter(entry => entry.kind === 'row').toSorted((a, b) => comparer(a.value[sorting.by], b.value[sorting.by])); if (sorting.reversed) { value.reverse(); } } if (grouping) { const implementation = grouping.with ?? defaultGroupingFunction(grouping.by); value = implementation(value as DataSetRowNode[]); } return value as DataSetNode[]; }); const mutations = createMemo(() => { // enumerate all values to make sure the memo is recalculated on any change Object.values(state.value).map(entry => Object.values(entry ?? {})); return deepDiff(state.snapshot, state.value).toArray(); }); const apply = (data: T[], mutations: Mutation[]) => { for (const mutation of mutations) { const path = mutation.key.split('.'); switch (mutation.kind) { case MutarionKind.Create: { let v: any = data; for (const part of path.slice(0, -1)) { if (v[part] === undefined) { v[part] = {}; } v = v[part]; } v[path.at(-1)!] = mutation.value; break; } case MutarionKind.Delete: { let v: any = data; for (const part of path.slice(0, -1)) { if (v === undefined) { break; } v = v[part]; } if (v !== undefined) { delete v[path.at(-1)!]; } break; } case MutarionKind.Update: { let v: any = data; for (const part of path.slice(0, -1)) { if (v === undefined) { break; } v = v[part]; } if (v !== undefined) { v[path.at(-1)!] = mutation.value; } break; } } } return data; }; createEffect(() => { const next = data(); const nextValue = apply(deepCopy(next), untrack(() => mutations())); setState('value', nextValue); setState('snapshot', next); }); const set: DataSet = { nodes, get value() { return state.value; }, mutations, get sorting() { return state.sorting; }, get grouping() { return state.grouping; }, mutate(index, prop, value) { setState('value', index, prop as any, value); }, mutateEach(setter) { setState('value', value => value.map(i => i === undefined ? undefined : setter(i))); }, remove(indices) { setState('value', value => value.map((item, i) => indices.includes(i) ? undefined : item)); }, insert(item, at) { if (at === undefined) { setState('value', state.value.length, item); } else { } }, sort(options) { setState('sorting', options); return set; }, group(options) { setState('grouping', options) return set; }, }; return set; };