diff --git a/src/components/table/dataset.ts b/src/components/table/dataset.ts index 193d956..270bbaf 100644 --- a/src/components/table/dataset.ts +++ b/src/components/table/dataset.ts @@ -1,13 +1,13 @@ -export type RowNode = { kind: 'row', key: string, value: T } -export type GroupNode = { kind: 'group', key: string, groupedBy: keyof T, nodes: Node[] }; -export type Node = RowNode | GroupNode; +export type DataSetRowNode = { kind: 'row', key: string, value: T } +export type DataSetGroupNode = { kind: 'group', key: string, groupedBy: keyof T, nodes: DataSetNode[] }; +export type DataSetNode = DataSetRowNode | DataSetGroupNode; -export type DataSet> = Node[]; +export type DataSet> = DataSetNode[]; -export const createDataSet = >(data: T[]): Node[] => { - return Object.entries(data).map>(([key, value]) => ({ kind: 'row', key, value })); +export const createDataSet = >(data: T[]): DataSetNode[] => { + return Object.entries(data).map>(([key, value]) => ({ kind: 'row', key, value })); }; type SortingFunction = (a: T, b: T) => -1 | 0 | 1; @@ -22,6 +22,6 @@ export const toSorted = >(dataSet: DataSet, sor return sorted; }; -type GroupingFunction = (nodes: RowNode[]) => Node[]; +type GroupingFunction = (nodes: DataSetRowNode[]) => DataSetNode[]; type GroupOptions> = { by: keyof T, with: GroupingFunction }; export const toGrouped = >(dataSet: DataSet, group: GroupOptions): DataSet => group.with(dataSet as any); \ No newline at end of file diff --git a/src/components/table/index.tsx b/src/components/table/index.tsx index 7d858b2..2e4bba6 100644 --- a/src/components/table/index.tsx +++ b/src/components/table/index.tsx @@ -1,5 +1,5 @@ export type { Column } from './table'; -export type { DataSet, GroupNode, RowNode, Node } from './dataset'; +export type { DataSet, DataSetGroupNode, DataSetRowNode, DataSetNode } from './dataset'; export { SelectionMode, Table } from './table'; export { createDataSet, toSorted, toGrouped } from './dataset'; \ No newline at end of file diff --git a/src/components/table/table.tsx b/src/components/table/table.tsx index 8d52f07..6191294 100644 --- a/src/components/table/table.tsx +++ b/src/components/table/table.tsx @@ -1,6 +1,6 @@ import { Accessor, createContext, createEffect, createMemo, createSignal, For, JSX, Match, Show, Switch, useContext } from "solid-js"; import { selectable, SelectionProvider, useSelection } from "~/features/selectable"; -import { type RowNode, type GroupNode, type Node, createDataSet, toSorted, toGrouped } from './dataset'; +import { DataSetRowNode, DataSetGroupNode, DataSetNode, createDataSet, toSorted, toGrouped } from './dataset'; import css from './table.module.css'; import { createStore } from "solid-js/store"; import { FaSolidSort, FaSolidSortDown, FaSolidSortUp } from "solid-icons/fa"; @@ -11,15 +11,27 @@ export type Column = { id: keyof T, label: string, sortable?: boolean, - readonly groupBy?: (rows: RowNode[]) => Node[], + readonly groupBy?: (rows: DataSetRowNode[]) => DataSetNode[], }; +type SelectionItem = { key: string, value: Accessor, element: WeakRef }; + +export interface TableApi> { + readonly selection: Accessor[]>; + readonly rows: Accessor; + readonly columns: Accessor[]>; + selectAll(): void; + clear(): void; +} + const TableContext = createContext<{ + readonly rows: Accessor, readonly columns: Accessor[]>, + readonly selection: Accessor, readonly selectionMode: Accessor, readonly groupBy: Accessor, readonly sort: Accessor<{ by: string, reversed?: boolean } | undefined>, - readonly cellRenderers: Accessor JSX.Element>>, + readonly cellRenderers: Accessor JSX.Element>>, setSort(setter: (current: { by: string, reversed?: boolean } | undefined) => { by: string, reversed: boolean } | undefined): void; }>(); @@ -27,8 +39,8 @@ const TableContext = createContext<{ const useTable = () => useContext(TableContext)! function defaultGroupingFunction(groupBy: keyof T) { - return (nodes: RowNode[]): Node[] => Object.entries(Object.groupBy>(nodes, r => r.value[groupBy])) - .map>(([key, nodes]) => ({ kind: 'group', key, groupedBy: groupBy, nodes: nodes! })); + return (nodes: DataSetRowNode[]): DataSetNode[] => Object.entries(Object.groupBy>(nodes, r => r.value[groupBy])) + .map>(([key, nodes]) => ({ kind: 'group', key, groupedBy: groupBy, nodes: nodes! })); } export enum SelectionMode { @@ -47,11 +59,11 @@ type TableProps> = { }, selectionMode?: SelectionMode, children?: { [K in keyof T]?: (cell: { value: T[K] }) => JSX.Element }, + api?: (api: TableApi) => any, }; export function Table>(props: TableProps) { - const [selection, setSelection] = createSignal([]); - + const [selection, setSelection] = createSignal([]); const [state, setState] = createStore({ sort: props.sort ? { by: props.sort.by as string, reversed: props.sort.reversed } : undefined, }); @@ -60,13 +72,16 @@ export function Table>(props: TableProps) { setState('sort', props.sort ? { by: props.sort.by as string, reversed: props.sort.reversed } : undefined); }); + const rows = createMemo(() => props.rows ?? []); const columns = createMemo[]>(() => props.columns ?? []); const selectionMode = createMemo(() => props.selectionMode ?? SelectionMode.None); const groupBy = createMemo(() => props.groupBy as string | undefined); const cellRenderers = createMemo(() => props.children ?? {}); const context = { + rows, columns, + selection, selectionMode, groupBy, sort: createMemo(() => state.sort), @@ -78,8 +93,10 @@ export function Table>(props: TableProps) { }; return - - + + + + ; }; @@ -91,7 +108,7 @@ function InnerTable>(props: InnerTableProps) { const selectable = createMemo(() => table.selectionMode() !== SelectionMode.None); const columnCount = createMemo(() => table.columns().length); - const nodes = createMemo[]>(() => { + const nodes = createMemo[]>(() => { const columns = table.columns(); const groupBy = table.groupBy(); const sort = table.sort(); @@ -121,6 +138,31 @@ function InnerTable>(props: InnerTableProps) { }; +function Api>(props: { api: undefined | ((api: TableApi) => any) }) { + const table = useTable(); + const selectionContext = useSelection>(); + + const api: TableApi = { + selection: createMemo(() => { + return selectionContext.selection(); + }), + rows: table.rows, + columns: table.columns, + selectAll() { + selectionContext.selectAll(); + }, + clear() { + selectionContext.clear(); + }, + }; + + createEffect(() => { + props.api?.(api); + }); + + return null; +}; + function Head>(props: {}) { const table = useTable(); const context = useSelection(); @@ -174,7 +216,7 @@ function Head>(props: {}) { ; }; -function Node>(props: { node: Node, depth: number, groupedBy?: keyof T }) { +function Node>(props: { node: DataSetNode, depth: number, groupedBy?: keyof T }) { return { row => @@ -201,12 +243,12 @@ function Row>(props: { key: string, value: T, dept { - ([k, v]) =>
{table.cellRenderers()[k]?.({ value: v }) ?? v}
+ ([k, value]) =>
{table.cellRenderers()[k]?.({ key: `${props.key}.${k}`, value }) ?? value}
}
; }; -function Group>(props: { key: string, groupedBy: keyof T, nodes: Node[], depth: number }) { +function Group>(props: { key: string, groupedBy: keyof T, nodes: DataSetNode[], depth: number }) { const table = useTable(); return
diff --git a/src/components/tabs.module.css b/src/components/tabs.module.css index 396b463..7c97ab8 100644 --- a/src/components/tabs.module.css +++ b/src/components/tabs.module.css @@ -61,44 +61,9 @@ } .tab { - position: absolute; - grid-area: 2 / 1 / span 1 / span 1; - inline-size: 100%; - block-size: 100%; - - &:not(.active) { - display: none; - } - - & > summary { - grid-row: 1 / 1; - - padding: var(--padding-s) var(--padding-m); - - &::marker { - content: none; - } - } - - &::details-content { - grid-area: 2 / 1 / span 1 / span var(--tab-count); - display: none; - grid: 100% / 100%; - inline-size: 100%; - block-size: 100%; - - overflow: auto; - } - - &[open] { - & > summary { - background-color: var(--surface-600); - } - - &::details-content { - display: grid; - } - } + display: contents; + background-color: var(--surface-600); + color: var(--text-1); } } diff --git a/src/components/tabs.tsx b/src/components/tabs.tsx index fe1e09c..3237249 100644 --- a/src/components/tabs.tsx +++ b/src/components/tabs.tsx @@ -95,19 +95,12 @@ export const Tab: ParentComponent<{ id: string, label: string, closable?: boolea const context = useTabs(); const resolved = children(() => props.children); const isActive = context.isActive(props.id); - const [ref, setRef] = createSignal(); - - // const isActive = context.register(props.id, props.label, { - // closable: props.closable ?? false, - // ref: ref, - // }); return
{resolved()} diff --git a/src/features/file/grid.tsx b/src/features/file/grid.tsx index d3c381c..a15bac8 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -1,22 +1,16 @@ import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, ParentComponent, Show, useContext } from "solid-js"; import { createStore, produce, unwrap } from "solid-js/store"; -import { SelectionProvider, useSelection, selectable } from "../selectable"; -import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities"; +import { debounce, deepCopy, deepDiff, Mutation, splitAt } from "~/utilities"; +import { DataSetRowNode, DataSetNode, SelectionMode, Table } from "~/components/table"; import css from './grid.module.css'; -selectable // prevents removal of import - -interface Leaf extends Record { } -export interface Entry extends Record { } - type Rows = Map>; -type SelectionItem = { key: string, value: Accessor>, element: WeakRef }; export interface GridContextType { - readonly rows: Accessor>>; + readonly rows: Accessor; readonly mutations: Accessor; - readonly selection: Accessor; - mutate(prop: string, lang: string, value: string): void; + // readonly selection: Accessor; + mutate(prop: string, value: string): void; remove(props: string[]): void; insert(prop: string): void; addColumn(name: string): void; @@ -35,11 +29,10 @@ export interface GridApi { const GridContext = createContext(); -const isLeaf = (entry: Entry | Leaf): entry is Leaf => Object.values(entry).some(v => typeof v === 'string'); const useGrid = () => useContext(GridContext)!; export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => { - const [selection, setSelection] = createSignal([]); + const [table, setTable] = createSignal(); const [state, setState] = createStore<{ rows: Record>, columns: string[], snapshot: Rows, numberOfRows: number }>({ rows: {}, columns: [], @@ -53,8 +46,25 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Rows, ap return deepDiff(state.snapshot, state.rows).toArray(); }); - const rows = createMemo(() => Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, unwrap(row)] as const))); - const columns = createMemo(() => state.columns); + + type Entry = { key: string, [lang: string]: string }; + + const groupBy = (rows: DataSetRowNode[]) => { + const group = (nodes: DataSetRowNode[]): DataSetNode[] => Object + .entries(Object.groupBy(nodes, r => r.key.split('.').at(0)!) as Record[]>) + .map>(([key, nodes]) => nodes.at(0)?.key === key + ? { ...nodes[0], key: nodes[0].value.key, value: { ...nodes[0].value, key: nodes[0].key } } + : ({ kind: 'group', key, groupedBy: 'key', nodes: group(nodes.map(n => ({ ...n, key: n.key.slice(key.length + 1) }))) }) + ); + + return group(rows.map(r => ({ ...r, key: r.value.key }))); + } + + const rows = createMemo(() => Object.entries(state.rows).map(([key, values]) => ({ key, ...values }))); + const columns = createMemo(() => [ + { id: 'key', label: 'Key', groupBy }, + ...state.columns.map(c => ({ id: c, label: c })), + ]); createEffect(() => { setState('rows', Object.fromEntries(deepCopy(props.rows).entries())); @@ -72,10 +82,12 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Rows, ap const ctx: GridContextType = { rows, mutations, - selection, + // selection, - mutate(prop: string, lang: string, value: string) { - setState('rows', prop, lang, value); + mutate(prop: string, value: string) { + const [key, lang] = splitAt(prop, prop.lastIndexOf('.')); + + setState('rows', key, lang, value); }, remove(props: string[]) { @@ -89,11 +101,7 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Rows, ap }, insert(prop: string) { - setState('rows', produce(rows => { - rows[prop] = Object.fromEntries(state.columns.slice(1).map(lang => [lang, ''])); - - return rows - })) + setState('rows', prop, Object.fromEntries(state.columns.map(lang => [lang, '']))); }, addColumn(name: string): void { @@ -107,145 +115,68 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Rows, ap }; return - - + - <_Grid class={props.class} columns={columns()} rows={rows()} /> - + { + Object.fromEntries(state.columns.map(c => [c, ({ key, value }: any) => { + return