refactor dataset to standalone feature and update components accordingly
This commit is contained in:
parent
3bd17306f2
commit
9ace9b9f4f
7 changed files with 109 additions and 34 deletions
|
@ -1,6 +1,7 @@
|
|||
import { Accessor, createContext, createEffect, createMemo, createSignal, JSX, useContext } from "solid-js";
|
||||
import { Mutation } from "~/utilities";
|
||||
import { SelectionMode, Table, Column as TableColumn, TableApi, DataSet, CellRenderer as TableCellRenderer } from "~/components/table";
|
||||
import { SelectionMode, Table, Column as TableColumn, TableApi, CellRenderer as TableCellRenderer } from "~/components/table";
|
||||
import { DataSet } from "~/features/dataset";
|
||||
import css from './grid.module.css';
|
||||
|
||||
export interface CellRenderer<T extends Record<string, any>, K extends keyof T> {
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
import { describe, expect, it } from "bun:test";
|
||||
import { createDataSet } from "./dataset";
|
||||
|
||||
interface DataEntry {
|
||||
id: string;
|
||||
name: string;
|
||||
amount: number;
|
||||
};
|
||||
const defaultData: DataEntry[] = [
|
||||
{ id: '1', name: 'a first name', amount: 30 },
|
||||
{ id: '2', name: 'a second name', amount: 20 },
|
||||
{ id: '3', name: 'a third name', amount: 10 },
|
||||
];
|
||||
|
||||
describe('dataset', () => {
|
||||
describe('createDataset', () => {
|
||||
it('can create an instance', async () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
const actual = createDataSet(defaultData);
|
||||
|
||||
// Assert
|
||||
expect(actual).toMatchObject({ data: defaultData })
|
||||
});
|
||||
|
||||
it('can sort by a property', async () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
const actual = createDataSet(defaultData, { sort: { by: 'amount', reversed: false } });
|
||||
|
||||
// Assert
|
||||
expect(actual.nodes()).toEqual([
|
||||
expect.objectContaining({ key: 2 }),
|
||||
expect.objectContaining({ key: 1 }),
|
||||
expect.objectContaining({ key: 0 }),
|
||||
])
|
||||
});
|
||||
|
||||
it('can group by a property', async () => {
|
||||
// Arrange
|
||||
|
||||
// Act
|
||||
const actual = createDataSet(defaultData, { group: { by: 'name' } });
|
||||
|
||||
// Assert
|
||||
expect(actual).toEqual(expect.objectContaining({ data: defaultData }))
|
||||
});
|
||||
|
||||
describe('mutate', () => {
|
||||
it('mutates the value', async () => {
|
||||
// Arrange
|
||||
const dataset = createDataSet(defaultData);
|
||||
|
||||
// Act
|
||||
dataset.mutate(0, 'amount', 100);
|
||||
|
||||
// Assert
|
||||
expect(dataset.value[0]!.amount).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mutateEach', () => {
|
||||
it('mutates all the entries', async () => {
|
||||
// Arrange
|
||||
const dataset = createDataSet(defaultData);
|
||||
|
||||
// Act
|
||||
dataset.mutateEach(entry => ({ ...entry, amount: entry.amount + 5 }));
|
||||
|
||||
// Assert
|
||||
expect(dataset.value).toEqual([
|
||||
expect.objectContaining({ amount: 35 }),
|
||||
expect.objectContaining({ amount: 25 }),
|
||||
expect.objectContaining({ amount: 15 }),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('remove', () => {
|
||||
it('removes the 2nd entry', async () => {
|
||||
// Arrange
|
||||
const dataset = createDataSet(defaultData);
|
||||
|
||||
// Act
|
||||
dataset.remove([1]);
|
||||
|
||||
// Assert
|
||||
expect(dataset.value[1]).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('insert', () => {
|
||||
it('adds an entry to the dataset', async () => {
|
||||
// Arrange
|
||||
const dataset = createDataSet(defaultData);
|
||||
|
||||
// Act
|
||||
dataset.insert({ id: '4', name: 'name', amount: 100 });
|
||||
|
||||
// Assert
|
||||
expect(dataset.value[3]).toEqual({ id: '4', name: 'name', amount: 100 });
|
||||
});
|
||||
});
|
||||
|
||||
describe('sort', () => {
|
||||
it('can set the sorting', async () => {
|
||||
// Arrange
|
||||
const dataset = createDataSet(defaultData);
|
||||
|
||||
// Act
|
||||
dataset.sort({ by: 'id', reversed: true });
|
||||
|
||||
// Assert
|
||||
expect(dataset.sorting).toEqual({ by: 'id', reversed: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('group', () => {
|
||||
it('can set the grouping', async () => {
|
||||
// Arrange
|
||||
const dataset = createDataSet(defaultData);
|
||||
|
||||
// Act
|
||||
dataset.group({ by: 'id' });
|
||||
|
||||
// Assert
|
||||
expect(dataset.grouping).toEqual({ by: 'id' });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,152 +0,0 @@
|
|||
import { Accessor, createEffect, createMemo } from "solid-js";
|
||||
import { createStore, NotWrappable, produce, StoreSetter, unwrap } from "solid-js/store";
|
||||
import { CustomPartial } from "solid-js/store/types/store.js";
|
||||
import { deepCopy, deepDiff, Mutation } from "~/utilities";
|
||||
|
||||
|
||||
export type DataSetRowNode<K, T> = { kind: 'row', key: K, value: T }
|
||||
export type DataSetGroupNode<K, T> = { kind: 'group', key: K, groupedBy: keyof T, nodes: DataSetNode<K, T>[] };
|
||||
export type DataSetNode<K, T> = DataSetRowNode<K, T> | DataSetGroupNode<K, T>;
|
||||
|
||||
export interface SortingFunction<T> {
|
||||
(a: T, b: T): -1 | 0 | 1;
|
||||
}
|
||||
export interface SortOptions<T extends Record<string, any>> {
|
||||
by: keyof T;
|
||||
reversed: boolean;
|
||||
with?: SortingFunction<T>;
|
||||
}
|
||||
|
||||
export interface GroupingFunction<K, T> {
|
||||
(nodes: DataSetRowNode<K, T>[]): DataSetNode<K, T>[];
|
||||
}
|
||||
export interface GroupOptions<T extends Record<string, any>> {
|
||||
by: keyof T;
|
||||
with?: GroupingFunction<number, T>;
|
||||
}
|
||||
interface DataSetState<T extends Record<string, any>> {
|
||||
value: (T | undefined)[];
|
||||
snapshot: (T | undefined)[];
|
||||
sorting?: SortOptions<T>;
|
||||
grouping?: GroupOptions<T>;
|
||||
}
|
||||
|
||||
export type Setter<T> =
|
||||
| T
|
||||
| CustomPartial<T>
|
||||
| ((prevState: T) => T | CustomPartial<T>);
|
||||
|
||||
export interface DataSet<T extends Record<string, any>> {
|
||||
data: T[];
|
||||
nodes: Accessor<DataSetNode<keyof T, T>[]>;
|
||||
mutations: Accessor<Mutation[]>;
|
||||
readonly value: (T | undefined)[];
|
||||
readonly sorting: SortOptions<T> | undefined;
|
||||
readonly grouping: GroupOptions<T> | undefined;
|
||||
|
||||
mutate<K extends keyof T>(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<SortOptions<T> | undefined>): DataSet<T>;
|
||||
group(options: Setter<GroupOptions<T> | undefined>): DataSet<T>;
|
||||
}
|
||||
|
||||
const defaultComparer = <T>(a: T, b: T) => a < b ? -1 : a > b ? 1 : 0;
|
||||
function defaultGroupingFunction<T>(groupBy: keyof T): GroupingFunction<number, T> {
|
||||
return <K>(nodes: DataSetRowNode<K, T>[]): DataSetNode<K, T>[] => Object.entries(Object.groupBy(nodes, r => r.value[groupBy] as PropertyKey))
|
||||
.map(([key, nodes]) => ({ kind: 'group', key, groupedBy: groupBy, nodes: nodes! } as DataSetGroupNode<K, T>));
|
||||
}
|
||||
|
||||
export const createDataSet = <T extends Record<string, any>>(data: T[], initialOptions?: { sort?: SortOptions<T>, group?: GroupOptions<T> }): DataSet<T> => {
|
||||
const [state, setState] = createStore<DataSetState<T>>({
|
||||
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<number, T>[] = state.value
|
||||
.map<DataSetRowNode<number, T> | 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<number, T>[]);
|
||||
}
|
||||
|
||||
return value as DataSetNode<keyof T, T>[];
|
||||
});
|
||||
|
||||
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 set: DataSet<T> = {
|
||||
data,
|
||||
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
|
||||
};
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
export type { Column, TableApi, CellRenderer, CellRenderers } from './table';
|
||||
export type { DataSet, DataSetGroupNode, DataSetRowNode, DataSetNode, SortingFunction, SortOptions, GroupingFunction, GroupOptions } from './dataset';
|
||||
export { SelectionMode, Table } from './table';
|
||||
export { createDataSet } from './dataset';
|
||||
export { SelectionMode, Table } from './table';
|
|
@ -1,6 +1,6 @@
|
|||
import { Accessor, createContext, createEffect, createMemo, createSignal, For, JSX, Match, Show, Switch, useContext } from "solid-js";
|
||||
import { selectable, SelectionItem, SelectionProvider, useSelection } from "~/features/selectable";
|
||||
import { DataSetRowNode, DataSetNode, DataSet } from './dataset';
|
||||
import { DataSetRowNode, DataSetNode, DataSet } from '~/features/dataset';
|
||||
import { FaSolidAngleDown, FaSolidSort, FaSolidSortDown, FaSolidSortUp } from "solid-icons/fa";
|
||||
import css from './table.module.css';
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue