finished refactoring. all the code is now way more clearly seperated and reusable

This commit is contained in:
Chris Kruining 2024-12-19 11:42:03 +01:00
parent c2b7a9ccf3
commit 998a788baa
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
13 changed files with 141 additions and 231 deletions

View file

@ -1,14 +1,14 @@
import { Accessor, createContext, createEffect, createMemo, createSignal, JSX, useContext } from "solid-js"; import { Accessor, createContext, createEffect, createMemo, createSignal, JSX, useContext } from "solid-js";
import { Mutation } from "~/utilities"; import { Mutation } from "~/utilities";
import { SelectionMode, Table, Column as TableColumn, TableApi, DataSet, CellRenderer } from "~/components/table"; import { SelectionMode, Table, Column as TableColumn, TableApi, DataSet, CellRenderer as TableCellRenderer } from "~/components/table";
import css from './grid.module.css'; import css from './grid.module.css';
export interface CellEditor<T extends Record<string, any>, K extends keyof T> { export interface CellRenderer<T extends Record<string, any>, K extends keyof T> {
(cell: Parameters<CellRenderer<T, K>>[0] & { mutate: (next: T[K]) => any }): JSX.Element; (cell: Parameters<TableCellRenderer<T, K>>[0] & { mutate: (next: T[K]) => any }): JSX.Element;
} }
export interface Column<T extends Record<string, any>> extends TableColumn<T> { export interface Column<T extends Record<string, any>> extends Omit<TableColumn<T>, 'renderer'> {
editor?: CellEditor<T, keyof T>; renderer?: CellRenderer<T, keyof T>;
} }
export interface GridApi<T extends Record<string, any>> extends TableApi<T> { export interface GridApi<T extends Record<string, any>> extends TableApi<T> {
@ -63,16 +63,16 @@ export function Grid<T extends Record<string, any>>(props: GridProps<T>) {
}, },
}; };
const cellEditors = createMemo(() => Object.fromEntries( const cellRenderers = createMemo(() => Object.fromEntries(
props.columns props.columns
.filter(c => c.editor !== undefined) .filter(c => c.renderer !== undefined)
.map(c => { .map(c => {
const Editor: CellRenderer<T, keyof T> = ({ row, column, value }) => { const Editor: CellRenderer<T, keyof T> = ({ row, column, value }) => {
const mutate = (next: T[keyof T]) => { const mutate = (next: T[keyof T]) => {
ctx.mutate(row, column, next); ctx.mutate(row, column, next);
}; };
return c.editor!({ row, column, value, mutate }); return c.renderer!({ row, column, value, mutate });
}; };
return [c.id, Editor] as const; return [c.id, Editor] as const;
@ -83,7 +83,7 @@ export function Grid<T extends Record<string, any>>(props: GridProps<T>) {
<Api api={props.api} table={table()} /> <Api api={props.api} table={table()} />
<Table api={setTable} class={`${css.grid} ${props.class}`} rows={rows()} columns={columns()} selectionMode={SelectionMode.Multiple}>{ <Table api={setTable} class={`${css.grid} ${props.class}`} rows={rows()} columns={columns()} selectionMode={SelectionMode.Multiple}>{
cellEditors() cellRenderers()
}</Table> }</Table>
</GridContext.Provider>; </GridContext.Provider>;
}; };

View file

@ -1,4 +1,4 @@
export type { DataSetRowNode, DataSetGroupNode, DataSetNode, SelectionMode, SortingFunction, SortOptions, GroupingFunction, GroupOptions } from '../table'; export type { DataSetRowNode, DataSetGroupNode, DataSetNode, SelectionMode, SortingFunction, SortOptions, GroupingFunction, GroupOptions } from '../table';
export type { GridApi, Column, CellEditor } from './grid'; export type { GridApi, Column, CellRenderer as CellEditor } from './grid';
export { Grid } from './grid'; export { Grid } from './grid';

View file

@ -1,5 +1,5 @@
import { Accessor, createEffect, createMemo } from "solid-js"; import { Accessor, createEffect, createMemo } from "solid-js";
import { createStore, NotWrappable, StoreSetter, unwrap } from "solid-js/store"; import { createStore, NotWrappable, produce, StoreSetter, unwrap } from "solid-js/store";
import { CustomPartial } from "solid-js/store/types/store.js"; import { CustomPartial } from "solid-js/store/types/store.js";
import { deepCopy, deepDiff, Mutation } from "~/utilities"; import { deepCopy, deepDiff, Mutation } from "~/utilities";
@ -38,13 +38,14 @@ export type Setter<T> =
export interface DataSet<T extends Record<string, any>> { export interface DataSet<T extends Record<string, any>> {
data: T[]; data: T[];
value: Accessor<DataSetNode<keyof T, T>[]>; nodes: Accessor<DataSetNode<keyof T, T>[]>;
value: Accessor<(T | undefined)[]>;
mutations: Accessor<Mutation[]>; mutations: Accessor<Mutation[]>;
sorting: Accessor<SortOptions<T> | undefined>; sorting: Accessor<SortOptions<T> | undefined>;
grouping: Accessor<GroupOptions<T> | undefined>; grouping: Accessor<GroupOptions<T> | undefined>;
// mutate<K extends keyof T>(index: number, value: T): void;
mutate<K extends keyof T>(index: number, prop: K, value: T[K]): void; mutate<K extends keyof T>(index: number, prop: K, value: T[K]): void;
mutateEach(setter: (value: T) => T): void;
remove(indices: number[]): void; remove(indices: number[]): void;
insert(item: T, at?: number): void; insert(item: T, at?: number): void;
@ -59,15 +60,14 @@ function defaultGroupingFunction<T>(groupBy: keyof T): GroupingFunction<number,
} }
export const createDataSet = <T extends Record<string, any>>(data: T[], initialOptions?: { sort?: SortOptions<T>, group?: GroupOptions<T> }): DataSet<T> => { export const createDataSet = <T extends Record<string, any>>(data: T[], initialOptions?: { sort?: SortOptions<T>, group?: GroupOptions<T> }): DataSet<T> => {
const nodes = data;
const [state, setState] = createStore<DataSetState<T>>({ const [state, setState] = createStore<DataSetState<T>>({
value: deepCopy(nodes), value: deepCopy(data),
snapshot: nodes, snapshot: data,
sorting: initialOptions?.sort, sorting: initialOptions?.sort,
grouping: initialOptions?.group, grouping: initialOptions?.group,
}); });
const value = createMemo(() => { const nodes = createMemo(() => {
const sorting = state.sorting; const sorting = state.sorting;
const grouping = state.grouping; const grouping = state.grouping;
@ -106,7 +106,8 @@ export const createDataSet = <T extends Record<string, any>>(data: T[], initialO
const set: DataSet<T> = { const set: DataSet<T> = {
data, data,
value, nodes,
value: createMemo(() => state.value),
mutations, mutations,
sorting, sorting,
grouping, grouping,
@ -115,6 +116,10 @@ export const createDataSet = <T extends Record<string, any>>(data: T[], initialO
setState('value', index, prop as any, value); setState('value', index, prop as any, value);
}, },
mutateEach(setter) {
setState('value', value => value.map(i => i === undefined ? undefined : setter(i)));
},
remove(indices) { remove(indices) {
setState('value', value => value.map((item, i) => indices.includes(i) ? undefined : item)); setState('value', value => value.map((item, i) => indices.includes(i) ? undefined : item));
}, },

View file

@ -163,6 +163,7 @@
& > .header { & > .header {
border-block-end-color: transparent; border-block-end-color: transparent;
animation: none;
& .cell { & .cell {
justify-content: start; justify-content: start;
@ -171,6 +172,7 @@
& > label { & > label {
--state: 0; --state: 0;
display: contents; display: contents;
cursor: pointer;
& input[type="checkbox"] { & input[type="checkbox"] {
display: none; display: none;
@ -181,6 +183,7 @@
transition: rotate .3s ease-in-out; transition: rotate .3s ease-in-out;
inline-size: 1em; inline-size: 1em;
aspect-ratio: 1; aspect-ratio: 1;
opacity: 1 !important;
} }
&:has(input:not(:checked)) { &:has(input:not(:checked)) {

View file

@ -6,17 +6,18 @@ import css from './table.module.css';
selectable; selectable;
export type Column<T> = { export type CellRenderer<T extends Record<string, any>, K extends keyof T> = (cell: { row: number, column: K, value: T[K] }) => JSX.Element;
export type CellRenderers<T extends Record<string, any>> = { [K in keyof T]?: CellRenderer<T, K> };
export interface Column<T extends Record<string, any>> {
id: keyof T, id: keyof T,
label: string, label: string,
sortable?: boolean, sortable?: boolean,
group?: string, group?: string,
renderer?: CellRenderer<T, keyof T>,
readonly groupBy?: (rows: DataSetRowNode<keyof T, T>[]) => DataSetNode<keyof T, T>[], readonly groupBy?: (rows: DataSetRowNode<keyof T, T>[]) => DataSetNode<keyof T, T>[],
}; };
export type CellRenderer<T extends Record<string, any>, K extends keyof T> = (cell: { row: number, column: K, value: T[K] }) => JSX.Element;
export type CellRenderers<T extends Record<string, any>> = { [K in keyof T]?: CellRenderer<T, K> };
export interface TableApi<T extends Record<string, any>> { export interface TableApi<T extends Record<string, any>> {
readonly selection: Accessor<SelectionItem<keyof T, T>[]>; readonly selection: Accessor<SelectionItem<keyof T, T>[]>;
readonly rows: Accessor<DataSet<T>>; readonly rows: Accessor<DataSet<T>>;
@ -96,7 +97,7 @@ function InnerTable<T extends Record<string, any>>(props: InnerTableProps<T>) {
<Head /> <Head />
<tbody class={css.main}> <tbody class={css.main}>
<For each={props.rows.value()}>{ <For each={props.rows.nodes()}>{
node => <Node node={node} depth={0} /> node => <Node node={node} depth={0} />
}</For> }</For>
</tbody> </tbody>

View file

@ -0,0 +1,19 @@
.textarea {
resize: vertical;
min-block-size: max(2em, 100%);
max-block-size: 50em;
background-color: var(--surface-600);
color: var(--text-1);
border-color: var(--text-2);
border-radius: var(--radii-s);
&:has(::spelling-error, ::grammar-error) {
border-color: var(--fail);
}
& ::spelling-error {
outline: 1px solid var(--fail);
text-decoration: yellow underline;
}
}

View file

@ -1,185 +1,70 @@
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, ParentComponent, Show, useContext } from "solid-js"; import { Accessor, Component, createEffect, createMemo, createSignal } from "solid-js";
import { createStore, produce, unwrap } from "solid-js/store"; import { debounce, Mutation } from "~/utilities";
import { debounce, deepCopy, deepDiff, Mutation, splitAt } from "~/utilities"; import { Column, GridApi as GridCompApi, Grid as GridComp } from "~/components/grid";
import { DataSetRowNode, DataSetNode, SelectionMode, Table } from "~/components/table"; import { createDataSet, DataSetNode, DataSetRowNode } from "~/components/table";
import css from './grid.module.css'; import css from "./grid.module.css"
type Rows = Map<string, Record<string, string>>;
export interface GridContextType {
readonly rows: Accessor<Rows>;
readonly mutations: Accessor<Mutation[]>;
// readonly selection: Accessor<SelectionItem[]>;
mutate(prop: string, value: string): void;
remove(props: string[]): void;
insert(prop: string): void;
addColumn(name: string): void;
}
export type Entry = { key: string } & { [lang: string]: string };
export interface GridApi { export interface GridApi {
readonly selection: Accessor<Record<string, Record<string, string>>>;
readonly rows: Accessor<Record<string, Record<string, string>>>;
readonly mutations: Accessor<Mutation[]>; readonly mutations: Accessor<Mutation[]>;
selectAll(): void; remove(indices: number[]): void;
clear(): void; addKey(key: string): void;
remove(keys: string[]): void; addLocale(locale: string): void;
insert(prop: string): void; };
addColumn(name: string): void;
}
const GridContext = createContext<GridContextType>(); const groupBy = (rows: DataSetRowNode<number, Entry>[]) => {
type R = DataSetRowNode<number, Entry> & { _key: string };
const useGrid = () => useContext(GridContext)!; const group = (nodes: R[]): DataSetNode<number, Entry>[] => Object
.entries(Object.groupBy(nodes, r => r._key.split('.').at(0)!) as Record<number, R[]>)
export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => { .map<any>(([key, nodes]) => nodes.at(0)?._key === key
const [table, setTable] = createSignal(); ? nodes[0]
const [state, setState] = createStore<{ rows: Record<string, Record<string, string>>, columns: string[], snapshot: Rows, numberOfRows: number }>({ : ({ kind: 'group', key, groupedBy: 'key', nodes: group(nodes.map(n => ({ ...n, _key: n._key.slice(key.length + 1) }))) })
rows: {},
columns: [],
snapshot: new Map,
numberOfRows: 0,
});
const mutations = createMemo(() => {
// enumerate all values to make sure the memo is recalculated on any change
Object.values(state.rows).map(entry => Object.values(entry));
return deepDiff(state.snapshot, state.rows).toArray();
});
type Entry = { key: string, [lang: string]: string };
const groupBy = (rows: DataSetRowNode<Entry>[]) => {
const group = (nodes: DataSetRowNode<Entry>[]): DataSetNode<Entry>[] => Object
.entries(Object.groupBy(nodes, r => r.key.split('.').at(0)!) as Record<string, DataSetRowNode<Entry>[]>)
.map<DataSetNode<Entry>>(([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 }))); return group(rows.map<R>(r => ({ ...r, _key: r.value.key }))) as any;
} }
const rows = createMemo(() => Object.entries(state.rows).map(([key, values]) => ({ key, ...values }))); export function Grid(props: { class?: string, rows: Entry[], api?: (api: GridApi) => any }) {
const columns = createMemo(() => [ const rows = createMemo(() => createDataSet<Entry>(props.rows, { group: { by: 'key', with: groupBy } }));
{ id: 'key', label: 'Key', groupBy }, const locales = createMemo(() => Object.keys(rows().value().at(0) ?? {}).filter(k => k !== 'key'));
...state.columns.map(c => ({ id: c, label: c })), const columns = createMemo<Column<Entry>[]>(() => [
{
id: 'key',
label: 'Key',
renderer: ({ value }) => value.split('.').at(-1),
},
...locales().map<Column<Entry>>(lang => ({
id: lang,
label: lang,
renderer: ({ row, column, value, mutate }) => {
const entry = rows().value()[row]!;
return <TextArea row={row} key={entry.key} lang={String(column)} value={value} oninput={e => mutate(e.data ?? '')} />;
},
}))
]); ]);
createEffect(() => { createEffect(() => {
setState('rows', Object.fromEntries(deepCopy(props.rows).entries())); const r = rows();
setState('snapshot', props.rows);
props.api?.({
mutations: r.mutations,
remove: r.remove,
addKey(key) {
r.insert({ key, ...Object.fromEntries(locales().map(l => [l, ''])) });
},
addLocale(locale) {
r.mutateEach(entry => ({ ...entry, [locale]: '' }));
},
});
}); });
createEffect(() => { return <GridComp rows={rows()} columns={columns()} />;
setState('columns', [...props.columns]);
});
createEffect(() => {
setState('numberOfRows', Object.keys(state.rows).length);
});
const ctx: GridContextType = {
rows,
mutations,
// selection,
mutate(prop: string, value: string) {
const [key, lang] = splitAt(prop, prop.lastIndexOf('.'));
setState('rows', key, lang, value);
},
remove(props: string[]) {
setState('rows', produce(rows => {
for (const prop of props) {
delete rows[prop];
}
return rows;
}));
},
insert(prop: string) {
setState('rows', prop, Object.fromEntries(state.columns.map(lang => [lang, ''])));
},
addColumn(name: string): void {
if (state.columns.includes(name)) {
return;
}
setState(produce(state => {
state.columns.push(name);
state.rows = Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, { ...row, [name]: '' }]));
return state;
}))
},
}; };
return <GridContext.Provider value={ctx}> const TextArea: Component<{ row: number, key: string, lang: string, value: string, oninput?: (event: InputEvent) => any }> = (props) => {
<Api api={props.api} table={table()} />
<Table api={setTable} class={props.class} rows={rows()} columns={columns()} groupBy="key" selectionMode={SelectionMode.Multiple}>{
Object.fromEntries(state.columns.map(c => [c, ({ key, value }: any) => {
return <TextArea key={key} value={value} oninput={(e) => ctx.mutate(key, e.data ?? '')} />;
}]))
}</Table>
</GridContext.Provider>;
};
const Api: Component<{ api: undefined | ((api: GridApi) => any), table?: any }> = (props) => {
const gridContext = useGrid();
const api = createMemo<GridApi | undefined>(() => {
const table = props.table;
if (!table) {
return;
}
return {
selection: createMemo(() => {
const selection = props.table?.selection() ?? [];
return Object.fromEntries(selection.map(({ key, value }) => [key, value()] as const));
}),
rows: createMemo(() => props.table?.rows ?? []),
mutations: gridContext.mutations,
selectAll() {
props.table.selectAll();
},
clear() {
props.table.clear();
},
remove(props: string[]) {
gridContext.remove(props);
},
insert(prop: string) {
gridContext.insert(prop);
},
addColumn(name: string): void {
gridContext.addColumn(name);
},
};
});
createEffect(() => {
const value = api();
if (value) {
props.api?.(value);
}
});
return null;
};
const TextArea: Component<{ key: string, value: string, oninput?: (event: InputEvent) => any }> = (props) => {
const [element, setElement] = createSignal<HTMLTextAreaElement>(); const [element, setElement] = createSignal<HTMLTextAreaElement>();
const key = createMemo(() => props.key.slice(0, props.key.lastIndexOf('.')));
const lang = createMemo(() => props.key.slice(props.key.lastIndexOf('.') + 1));
const resize = () => { const resize = () => {
const el = element(); const el = element();
@ -205,10 +90,11 @@ const TextArea: Component<{ key: string, value: string, oninput?: (event: InputE
return <textarea return <textarea
ref={setElement} ref={setElement}
class={css.textarea}
value={props.value} value={props.value}
lang={lang()} lang={props.lang}
placeholder={`${key()} in ${lang()}`} placeholder={`${props.key} in ${props.lang}`}
name={`${key()}:${lang()}`} name={`${props.row}[${props.lang}]`}
spellcheck={true} spellcheck={true}
wrap="soft" wrap="soft"
onkeyup={onKeyUp} onkeyup={onKeyUp}

View file

@ -1,4 +1,7 @@
.root { .root {
margin: 0;
padding: 0;
& > div { & > div {
display: contents; display: contents;
} }

View file

@ -5,7 +5,7 @@ import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/componen
import { Menu } from "~/features/menu"; import { Menu } from "~/features/menu";
import { Grid, load, useFiles } from "~/features/file"; import { Grid, load, useFiles } from "~/features/file";
import { Command, CommandType, Context, createCommand, Modifier, noop, useCommands } from "~/features/command"; import { Command, CommandType, Context, createCommand, Modifier, noop, useCommands } from "~/features/command";
import { GridApi } from "~/features/file/grid"; import { Entry, GridApi } from "~/features/file/grid";
import { Tab, Tabs } from "~/components/tabs"; import { Tab, Tabs } from "~/components/tabs";
import { isServer } from "solid-js/web"; import { isServer } from "solid-js/web";
import { Prompt, PromptApi } from "~/components/prompt"; import { Prompt, PromptApi } from "~/components/prompt";
@ -38,7 +38,8 @@ async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []):
}; };
interface Entries extends Map<string, Record<string, { value: string, handle: FileSystemFileHandle, id: string }>> { } // interface Entries extends Map<string, Record<string, { value: string, handle: FileSystemFileHandle, id: string }>> { };
interface Entries extends Map<string, { key: string, } & Record<string, { value: string, handle: FileSystemFileHandle, id: string }>> { };
export default function Edit(props: ParentProps) { export default function Edit(props: ParentProps) {
const filesContext = useFiles(); const filesContext = useFiles();
@ -105,26 +106,22 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
const files = tab.files(); const files = tab.files();
const mutations = tab.api()?.mutations() ?? []; const mutations = tab.api()?.mutations() ?? [];
// console.log(mutations); return mutations.flatMap((m): any => {
const [index, lang] = splitAt(m.key, m.key.indexOf('.'));
return mutations.flatMap(m => {
switch (m.kind) { switch (m.kind) {
case MutarionKind.Update: { case MutarionKind.Update: {
const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.')); const entry = entries.get(index as any)!;
return { kind: MutarionKind.Update, key: entry.key, lang, file: files.get(lang)! };
return { kind: MutarionKind.Update, key, lang, file: entries.get(key)?.[lang] };
} }
case MutarionKind.Create: { case MutarionKind.Create: {
if (typeof m.value === 'object') { if (typeof m.value === 'object') {
return Object.entries(m.value).map(([lang, value]) => { return Object.entries(m.value).map(([lang, value]) => ({ kind: MutarionKind.Create, key: m.key, lang, file: files.get(lang)!, value }));
return ({ kind: MutarionKind.Create, key: m.key, lang, file: files.get(lang)!, value });
});
} }
const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.')); const entry = entries.get(index as any)!;
return { kind: MutarionKind.Create, key: entry.key, lang, file: undefined, value: m.value };
return { kind: MutarionKind.Create, key, lang, file: undefined, value: m.value };
} }
case MutarionKind.Delete: { case MutarionKind.Delete: {
@ -226,6 +223,11 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
return existingFiles.concat(newFiles); return existingFiles.concat(newFiles);
}); });
// createEffect(() => {
// console.table(mutations());
// console.log(mutatedFiles(), mutatedData());
// });
createEffect(() => { createEffect(() => {
const directory = props.root; const directory = props.root;
@ -296,19 +298,17 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
return; return;
} }
api()?.insert(key); api()?.addKey(key);
}), }),
inserNewLanguage: createCommand('insert new language', async () => { inserNewLanguage: createCommand('insert new language', async () => {
const formData = await newLanguagePrompt()?.showModal(); const formData = await newLanguagePrompt()?.showModal();
const language = formData?.get('locale')?.toString(); const locale = formData?.get('locale')?.toString();
if (!language) { if (!locale) {
return; return;
} }
console.log(language); api()?.addLocale(locale);
api()?.addColumn(language);
}), }),
} as const; } as const;
@ -375,11 +375,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
<Tabs class={css.content} active={setActive} onClose={commands.closeTab}> <Tabs class={css.content} active={setActive} onClose={commands.closeTab}>
<For each={tabs()}>{ <For each={tabs()}>{
({ key, handle, setApi, setEntries }) => <Tab ({ key, handle, setApi, setEntries }) => <Tab id={key} label={handle.name} closable>
id={key}
label={handle.name}
closable
>
<Content directory={handle} api={setApi} entries={setEntries} /> <Content directory={handle} api={setApi} entries={setEntries} />
</Tab> </Tab>
}</For> }</For>
@ -389,8 +385,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<GridApi | undefined>, entries?: Setter<Entries> }> = (props) => { const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<GridApi | undefined>, entries?: Setter<Entries> }> = (props) => {
const [entries, setEntries] = createSignal<Entries>(new Map()); const [entries, setEntries] = createSignal<Entries>(new Map());
const [columns, setColumns] = createSignal<string[]>([]); const [rows, setRows] = createSignal<Entry[]>([]);
const [rows, setRows] = createSignal<Map<string, Record<string, string>>>(new Map);
const [api, setApi] = createSignal<GridApi>(); const [api, setApi] = createSignal<GridApi>();
createEffect(() => { createEffect(() => {
@ -420,7 +415,6 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<Gr
return { id, handle, lang, entries }; return { id, handle, lang, entries };
} }
); );
const languages = new Set(contents.map(c => c.lang));
const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]); const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]);
const merged = contents.reduce((aggregate, { id, handle, lang, entries }) => { const merged = contents.reduce((aggregate, { id, handle, lang, entries }) => {
@ -435,13 +429,12 @@ const Content: Component<{ directory: FileSystemDirectoryHandle, api?: Setter<Gr
return aggregate; return aggregate;
}, new Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>()); }, new Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>());
setColumns(languages.values().toArray()); setEntries(new Map(merged.entries().map(([key, langs], i) => [i.toString(), { key, ...langs }])) as Entries);
setEntries(merged); setRows(merged.entries().map(([key, langs]) => ({ key, ...Object.fromEntries(Object.entries(langs).map(([lang, { value }]) => [lang, value])) } as Entry)).toArray());
setRows(new Map(merged.entries().map(([key, langs]) => [key, Object.fromEntries(Object.entries(langs).map(([lang, { value }]) => [lang, value]))] as const)));
})(); })();
}); });
return <Grid columns={columns()} rows={rows()} api={setApi} />; return <Grid rows={rows()} api={setApi} />;
}; };
const Blank: Component<{ open: CommandType }> = (props) => { const Blank: Component<{ open: CommandType }> = (props) => {

View file

@ -26,37 +26,37 @@ export default function GridExperiment() {
id: 'name', id: 'name',
label: 'Name', label: 'Name',
sortable: true, sortable: true,
editor, renderer: editor,
}, },
{ {
id: 'email', id: 'email',
label: 'Email', label: 'Email',
sortable: true, sortable: true,
editor, renderer: editor,
}, },
{ {
id: 'address', id: 'address',
label: 'Address', label: 'Address',
sortable: true, sortable: true,
editor, renderer: editor,
}, },
{ {
id: 'currency', id: 'currency',
label: 'Currency', label: 'Currency',
sortable: true, sortable: true,
editor, renderer: editor,
}, },
{ {
id: 'phone', id: 'phone',
label: 'Phone', label: 'Phone',
sortable: true, sortable: true,
editor, renderer: editor,
}, },
{ {
id: 'country', id: 'country',
label: 'Country', label: 'Country',
sortable: true, sortable: true,
editor, renderer: editor,
}, },
]; ];