[Feature] Add language #19
13 changed files with 141 additions and 231 deletions
|
@ -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>;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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';
|
|
@ -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));
|
||||||
},
|
},
|
||||||
|
|
|
@ -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)) {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
.root {
|
.root {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
display: contents;
|
display: contents;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue