- moved grid rendering to feature
- re implemented file loading for order preservation - added styling for grid - implemented row selection POC
This commit is contained in:
parent
70c15c4094
commit
ebd8ff8c1d
10 changed files with 897 additions and 143 deletions
18
src/app.css
18
src/app.css
|
@ -1,3 +1,5 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght,GRAD@8..144,400,45;8..144,400,50;8..144,1000,0&family=Roboto+Serif:opsz,GRAD@8..144,71&display=swap");
|
||||
|
||||
:root {
|
||||
--surface-1: #eee;
|
||||
--surface-2: #f8f8f8;
|
||||
|
@ -7,6 +9,11 @@
|
|||
|
||||
color: var(--text);
|
||||
accent-color: var(--primary);
|
||||
|
||||
--info: oklch(.71 .17 249);
|
||||
--fail: oklch(.64 .21 25.3);
|
||||
--warn: oklch(.82 .18 78.9);
|
||||
--succ: oklch(.86 .28 150);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
|
@ -15,7 +22,13 @@
|
|||
--surface-2: #383838;
|
||||
--surface-3: #444;
|
||||
--text: #eee;
|
||||
|
||||
--primary: #6be8d6;
|
||||
|
||||
--info: oklch(.71 .17 249);
|
||||
--fail: oklch(.64 .21 25.3);
|
||||
--warn: oklch(.82 .18 78.9);
|
||||
--succ: oklch(.86 .28 150);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -26,18 +39,19 @@ html {
|
|||
}
|
||||
|
||||
body {
|
||||
position: relative;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
overflow: clip;
|
||||
|
||||
display: grid;
|
||||
grid: auto 1fr / 100%;
|
||||
grid: auto minmax(0, 1fr) / 100%;
|
||||
|
||||
background-color: var(--surface-1);
|
||||
|
||||
margin: 0;
|
||||
|
||||
font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||
font-family: Roboto Flex, sans-serif;
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
|
|
106
src/features/file/grid.css
Normal file
106
src/features/file/grid.css
Normal file
|
@ -0,0 +1,106 @@
|
|||
.table {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: 2em minmax(10em, max-content) repeat(var(--columns), auto);
|
||||
align-content: start;
|
||||
padding-inline: 1px;
|
||||
margin-inline: -1px;
|
||||
|
||||
block-size: 100%;
|
||||
overflow: clip auto;
|
||||
|
||||
background-color: var(--surface-1);
|
||||
|
||||
& input[type="checkbox"] {
|
||||
margin: .1em;
|
||||
}
|
||||
|
||||
|
||||
& textarea {
|
||||
resize: vertical;
|
||||
min-block-size: 2em;
|
||||
max-block-size: 50em;
|
||||
}
|
||||
|
||||
& .cell {
|
||||
display: grid;
|
||||
place-content: center stretch;
|
||||
padding: .5em;
|
||||
}
|
||||
|
||||
& > :is(header, main, footer) {
|
||||
grid-column: span calc(2 + var(--columns));
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
& > header {
|
||||
position: sticky;
|
||||
inset-block-start: 0;
|
||||
background-color: inherit;
|
||||
}
|
||||
|
||||
& label {
|
||||
--bg: var(--text);
|
||||
--alpha: 0;
|
||||
grid-column: span calc(2 + var(--columns));
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
border: 1px solid transparent;
|
||||
background-color: color(from var(--bg) srgb r g b / var(--alpha));
|
||||
|
||||
&:has(> .cell > :checked) {
|
||||
--bg: var(--info);
|
||||
--alpha: .1;
|
||||
border-color: var(--bg);
|
||||
|
||||
& span {
|
||||
font-variation-settings: 'GRAD' 1000;
|
||||
}
|
||||
|
||||
& + :has(> .cell> :checked) {
|
||||
border-block-start-color: transparent;
|
||||
}
|
||||
|
||||
&:has(+ label > .cell > :checked) {
|
||||
border-block-end-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
--alpha: .2 !important;
|
||||
}
|
||||
}
|
||||
|
||||
& details {
|
||||
display: contents;
|
||||
|
||||
&::details-content {
|
||||
grid-column: span calc(2 + var(--columns));
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
&:not([open])::details-content {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > summary {
|
||||
grid-column: 2 / span calc(1 + var(--columns));
|
||||
|
||||
&.cell {
|
||||
padding-inline-start: calc((var(--depth) + 1) * 1em);
|
||||
}
|
||||
}
|
||||
|
||||
& > label > .cell > span {
|
||||
padding-inline-start: calc(var(--depth) * 1.25em);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@property --depth {
|
||||
syntax: "<number>";
|
||||
inherits: false;
|
||||
initial-value: 0;
|
||||
}
|
134
src/features/file/grid.tsx
Normal file
134
src/features/file/grid.tsx
Normal file
|
@ -0,0 +1,134 @@
|
|||
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, ParentComponent, Show, useContext } from "solid-js";
|
||||
import './grid.css';
|
||||
import { createStore } from "solid-js/store";
|
||||
|
||||
interface Leaf extends Record<string, string> { }
|
||||
export interface Entry extends Record<string, Entry | Leaf> { }
|
||||
|
||||
interface SelectionContextType {
|
||||
rowCount(): number;
|
||||
selection(): string[];
|
||||
isSelected(key: string): boolean,
|
||||
selectAll(select: boolean): void;
|
||||
select(key: string, select: true): void;
|
||||
}
|
||||
|
||||
const SelectionContext = createContext<SelectionContextType>();
|
||||
|
||||
const isLeaf = (entry: Entry | Leaf): entry is Leaf => Object.values(entry).some(v => typeof v === 'string');
|
||||
const useSelection = () => useContext(SelectionContext)!;
|
||||
|
||||
const SelectionProvider: ParentComponent<{ rows: Map<string, { [lang: string]: { value: string, handle: FileSystemFileHandle } }>, context?: (ctx: SelectionContextType) => any }> = (props) => {
|
||||
const [state, setState] = createStore<{ selection: string[] }>({ selection: [] });
|
||||
|
||||
const rowKeys = createMemo(() => {
|
||||
return Array.from(props.rows?.keys());
|
||||
});
|
||||
|
||||
const context = {
|
||||
rowCount() {
|
||||
return rowKeys().length;
|
||||
},
|
||||
selection() {
|
||||
return state.selection;
|
||||
},
|
||||
isSelected(key: string) {
|
||||
return state.selection.includes(key);
|
||||
},
|
||||
selectAll(selected: boolean) {
|
||||
setState('selection', selected ? rowKeys() : []);
|
||||
},
|
||||
select(key: string, select: true) {
|
||||
setState('selection', selection => {
|
||||
if (select) {
|
||||
return [...selection, key];
|
||||
}
|
||||
|
||||
return selection.toSpliced(selection.indexOf(key), 1);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
props.context?.(context)
|
||||
});
|
||||
|
||||
return <SelectionContext.Provider value={context}>
|
||||
{props.children}
|
||||
</SelectionContext.Provider>;
|
||||
};
|
||||
|
||||
export const Grid: Component<{ columns: string[], rows: Map<string, { [lang: string]: { value: string, handle: FileSystemFileHandle } }>, context?: (ctx: SelectionContextType) => any }> = (props) => {
|
||||
const columnCount = createMemo(() => props.columns.length - 1);
|
||||
const root = createMemo<Entry>(() => {
|
||||
return Object.fromEntries(props.rows.entries().map(([key, value]) => [key, Object.fromEntries(Object.entries(value).map(([lang, { value }]) => [lang, value]))]));
|
||||
});
|
||||
|
||||
return <section class="table" style={{ '--columns': columnCount() }}>
|
||||
<SelectionProvider rows={props.rows} context={props.context}>
|
||||
<Head headers={props.columns} />
|
||||
|
||||
<main>
|
||||
<Row entry={root()} />
|
||||
</main>
|
||||
</SelectionProvider>
|
||||
</section>
|
||||
};
|
||||
|
||||
const Head: Component<{ headers: string[] }> = (props) => {
|
||||
const context = useSelection();
|
||||
|
||||
return <header>
|
||||
<div class="cell">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={context.selection().length > 0 && context.selection().length === context.rowCount()}
|
||||
indeterminate={context.selection().length !== 0 && context.selection().length !== context.rowCount()}
|
||||
on:input={(e: InputEvent) => context.selectAll(e.target.checked)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<For each={props.headers}>{
|
||||
header => <span class="cell">{header}</span>
|
||||
}</For>
|
||||
</header>;
|
||||
};
|
||||
|
||||
const Row: Component<{ entry: Entry, path?: string[] }> = (props) => {
|
||||
return <For each={Object.entries(props.entry)}>{
|
||||
([key, value]) => {
|
||||
const values = Object.values(value);
|
||||
const path = [...(props.path ?? []), key];
|
||||
const k = path.join('.');
|
||||
const context = useSelection();
|
||||
|
||||
return <Show when={isLeaf(value)} fallback={<Group key={key} entry={value as Entry} path={path} />}>
|
||||
<label for={k}>
|
||||
<div class="cell">
|
||||
<input type="checkbox" id={k} checked={context.isSelected(k)} on:input={(e: InputEvent) => context.select(k, e.target.checked)} />
|
||||
</div>
|
||||
|
||||
<div class="cell">
|
||||
<span style={{ '--depth': path.length - 1 }}>{key}</span>
|
||||
</div>
|
||||
|
||||
<For each={values}>{
|
||||
value =>
|
||||
<div class="cell"><textarea value={value} on:keyup={(e) => {
|
||||
e.target.style.blockSize = `1px`;
|
||||
e.target.style.blockSize = `${11 + e.target.scrollHeight}px`;
|
||||
}} /></div>
|
||||
}</For>
|
||||
</label>
|
||||
</Show>;
|
||||
}
|
||||
}</For>
|
||||
};
|
||||
|
||||
const Group: Component<{ key: string, entry: Entry, path: string[] }> = (props) => {
|
||||
return <details open>
|
||||
<summary class="cell" style={{ '--depth': props.path.length - 1 }}>{props.key}</summary>
|
||||
|
||||
<Row entry={props.entry} path={props.path} />
|
||||
</details>;
|
||||
};
|
|
@ -1,21 +1,22 @@
|
|||
import Dexie, { EntityTable } from "dexie";
|
||||
import { Component, createContext, useContext } from "solid-js";
|
||||
import { createContext, useContext } from "solid-js";
|
||||
import { isServer } from "solid-js/web";
|
||||
import * as json from './parser/json';
|
||||
|
||||
type Handle = FileSystemFileHandle|FileSystemDirectoryHandle;
|
||||
type Handle = FileSystemFileHandle | FileSystemDirectoryHandle;
|
||||
|
||||
interface File {
|
||||
interface FileEntity {
|
||||
name: string;
|
||||
handle: Handle;
|
||||
}
|
||||
|
||||
type Store = Dexie & {
|
||||
files: EntityTable<File, 'name'>;
|
||||
files: EntityTable<FileEntity, 'name'>;
|
||||
};
|
||||
|
||||
interface FilesContextType {
|
||||
set(name: string, handle: Handle): Promise<void>;
|
||||
get(name: string): Promise<Handle|undefined>;
|
||||
get(name: string): Promise<Handle | undefined>;
|
||||
list(): Promise<Handle[]>;
|
||||
}
|
||||
|
||||
|
@ -37,20 +38,20 @@ const clientContext = (): FilesContextType => {
|
|||
},
|
||||
async list() {
|
||||
const files = await db.files.toArray();
|
||||
|
||||
|
||||
return files.map(f => f.handle)
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
const serverContext = (): FilesContextType => ({
|
||||
set(){
|
||||
set() {
|
||||
return Promise.resolve();
|
||||
},
|
||||
get(name: string){
|
||||
get(name: string) {
|
||||
return Promise.resolve(undefined);
|
||||
},
|
||||
list(){
|
||||
list() {
|
||||
return Promise.resolve<Handle[]>([]);
|
||||
},
|
||||
});
|
||||
|
@ -63,6 +64,13 @@ export const FilesProvider = (props) => {
|
|||
|
||||
export const useFiles = () => useContext(FilesContext)!;
|
||||
|
||||
export const open = () => {
|
||||
export const load = (file: File): Promise<Map<string, string> | undefined> => {
|
||||
switch (file.type) {
|
||||
case 'application/json': return json.load(file.stream())
|
||||
|
||||
};
|
||||
default: return Promise.resolve(undefined);
|
||||
}
|
||||
};
|
||||
|
||||
export { Grid } from './grid';
|
||||
export type { Entry } from './grid';
|
199
src/features/file/parser/json.ts
Normal file
199
src/features/file/parser/json.ts
Normal file
|
@ -0,0 +1,199 @@
|
|||
|
||||
export async function load(stream: ReadableStream<Uint8Array>): Promise<Map<string, string>> {
|
||||
return new Map(await Array.fromAsync(parse(stream), ({ key, value }) => [key, value]));
|
||||
}
|
||||
|
||||
interface Entry {
|
||||
key: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
(token: Token): State;
|
||||
entry?: Entry
|
||||
}
|
||||
|
||||
const states = {
|
||||
none(): State {
|
||||
return (token: Token) => {
|
||||
if (token.kind === 'braceOpen') {
|
||||
return states.object();
|
||||
}
|
||||
|
||||
return states.none;
|
||||
};
|
||||
},
|
||||
object({ path = [], expect = 'key' }: Partial<{ path: string[], expect: 'key' | 'colon' | 'value' }> = {}): State {
|
||||
return (token: Token) => {
|
||||
switch (expect) {
|
||||
case 'key': {
|
||||
if (token.kind === 'braceClose') {
|
||||
return states.object({
|
||||
path: path.slice(0, -1),
|
||||
expect: 'key',
|
||||
});
|
||||
}
|
||||
else if (token.kind === 'string') {
|
||||
return states.object({
|
||||
path: [...path, token.value],
|
||||
expect: 'colon'
|
||||
});
|
||||
}
|
||||
|
||||
return states.error(`Expected a key, got ${token.kind} instead`);
|
||||
}
|
||||
|
||||
case 'colon': {
|
||||
if (token.kind !== 'colon') {
|
||||
return states.error(`Expected a ':', got ${token.kind} instead`);
|
||||
}
|
||||
|
||||
return states.object({
|
||||
path,
|
||||
expect: 'value'
|
||||
});
|
||||
}
|
||||
|
||||
case 'value': {
|
||||
if (token.kind === 'braceOpen') {
|
||||
return states.object({
|
||||
path,
|
||||
expect: 'key',
|
||||
});
|
||||
}
|
||||
else if (token.kind === 'string') {
|
||||
const next = states.object({
|
||||
path: path.slice(0, -1),
|
||||
expect: 'key',
|
||||
});
|
||||
|
||||
next.entry = { key: path.join('.'), value: token.value };
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
return states.error(`Invalid value type found '${token.kind}'`);
|
||||
}
|
||||
}
|
||||
|
||||
return states.none();
|
||||
}
|
||||
},
|
||||
error(message: string): State {
|
||||
throw new Error(message);
|
||||
|
||||
return states.none();
|
||||
},
|
||||
} as const;
|
||||
|
||||
async function* parse(stream: ReadableStream<Uint8Array>): AsyncGenerator<any, void, unknown> {
|
||||
let state = states.none();
|
||||
|
||||
for await (const token of take(tokenize(read(toGenerator(stream))), 100)) {
|
||||
try {
|
||||
state = state(token);
|
||||
}
|
||||
catch (e) {
|
||||
console.error(e);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (state.entry) {
|
||||
yield state.entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function* take<T>(iterable: AsyncIterable<T>, numberToTake: number): AsyncGenerator<T, void, unknown> {
|
||||
let i = 0;
|
||||
for await (const entry of iterable) {
|
||||
yield entry;
|
||||
|
||||
i++;
|
||||
|
||||
if (i === numberToTake) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type Token = { start: number, length: number } & (
|
||||
| { kind: 'braceOpen' }
|
||||
| { kind: 'braceClose' }
|
||||
| { kind: 'colon' }
|
||||
| { kind: 'string', value: string }
|
||||
);
|
||||
|
||||
async function* tokenize(characters: AsyncIterable<number>): AsyncGenerator<Token, void, unknown> {
|
||||
let buffer: string = '';
|
||||
let clearBuffer = false;
|
||||
let start = 0;
|
||||
let i = 0;
|
||||
|
||||
for await (const character of characters) {
|
||||
if (buffer.length === 0) {
|
||||
start = i;
|
||||
}
|
||||
|
||||
buffer += String.fromCharCode(character);
|
||||
const length = buffer.length;
|
||||
|
||||
if (buffer === '{') {
|
||||
yield { kind: 'braceOpen', start, length };
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer === '}') {
|
||||
yield { kind: 'braceClose', start, length };
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer === ':') {
|
||||
yield { kind: 'colon', start, length };
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer.length > 1 && buffer.startsWith('"') && buffer.endsWith('"') && buffer.at(-2) !== '\\') {
|
||||
yield { kind: 'string', start, length, value: buffer.slice(1, buffer.length - 1) };
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer === ',') {
|
||||
clearBuffer = true;
|
||||
}
|
||||
else if (buffer.trim() === '') {
|
||||
clearBuffer = true;
|
||||
}
|
||||
|
||||
if (clearBuffer) {
|
||||
buffer = '';
|
||||
clearBuffer = false;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
async function* read(chunks: AsyncIterable<Uint8Array>): AsyncGenerator<number, void, unknown> {
|
||||
for await (const chunk of chunks) {
|
||||
for (const character of chunk) {
|
||||
yield character;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function* toGenerator<T>(stream: ReadableStream<T>): AsyncGenerator<T, void, unknown> {
|
||||
const reader = stream.getReader();
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
|
@ -91,7 +91,7 @@ const Item: Component<ItemProps> = (props) => {
|
|||
return mergeProps(props, {
|
||||
id,
|
||||
get children() {
|
||||
return childItems();
|
||||
return childItems.toArray();
|
||||
}
|
||||
}) as unknown as JSX.Element;
|
||||
}
|
||||
|
|
|
@ -1,27 +0,0 @@
|
|||
.table {
|
||||
display: grid;
|
||||
overflow: auto;
|
||||
grid-template-columns: 1em max-content repeat(var(--columns), auto);
|
||||
gap: .5em;
|
||||
|
||||
block-size: 100%;
|
||||
overflow: clip auto;
|
||||
|
||||
& > :is(header, main, footer) {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
& details {
|
||||
display: contents;
|
||||
|
||||
&::details-content {
|
||||
grid-column: span calc(2 + var(--columns));
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
}
|
||||
|
||||
& > summary {
|
||||
grid-column: 2 / span calc(1 + var(--columns));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +1,11 @@
|
|||
import { createCommand, Menu, Modifier } from "~/features/menu";
|
||||
import { Component, createEffect, createMemo, createResource, createSignal, For, onMount, Show } from "solid-js";
|
||||
import { useFiles } from "~/features/file";
|
||||
import { createEffect, createResource, createSignal, onMount } from "solid-js";
|
||||
import { Entry, Grid, load, useFiles } from "~/features/file";
|
||||
import "./edit.css";
|
||||
|
||||
interface Entry extends Record<string, Entry | string> { }
|
||||
interface RawEntry extends Record<string, RawEntry | string> { }
|
||||
|
||||
interface Leaf extends Record<string, string> { }
|
||||
interface Entry2 extends Record<string, Entry2 | Leaf> { }
|
||||
|
||||
async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): AsyncGenerator<{ lang: string, entries: Entry }, void, never> {
|
||||
async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): AsyncGenerator<{ handle: FileSystemFileHandle, path: string[], lang: string, entries: Map<string, string> }, void, never> {
|
||||
for await (const handle of directory.values()) {
|
||||
if (handle.kind === 'directory') {
|
||||
yield* walk(handle, [...path, handle.name]);
|
||||
|
@ -21,30 +18,21 @@ async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []):
|
|||
}
|
||||
|
||||
const file = await handle.getFile();
|
||||
|
||||
if (file.type !== 'application/json') {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lang = file.name.split('.').at(0)!;
|
||||
const text = await file.text();
|
||||
const root: Entry = {};
|
||||
const entries = await load(file);
|
||||
|
||||
let current: Entry = root;
|
||||
for (const key of path) {
|
||||
current[key] = {};
|
||||
|
||||
current = current[key];
|
||||
if (entries !== undefined) {
|
||||
yield { handle, path, lang, entries };
|
||||
}
|
||||
Object.assign(current, JSON.parse(text));
|
||||
|
||||
yield { lang, entries: root };
|
||||
}
|
||||
};
|
||||
|
||||
export default function Edit(props) {
|
||||
const files = useFiles();
|
||||
const [root, { mutate, refetch }] = createResource(() => files.get('root'));
|
||||
const [columns, setColumns] = createSignal(['these', 'are', 'some', 'columns']);
|
||||
const [rows, setRows] = createSignal<Map<string, { [lang: string]: { value: string, handle: FileSystemFileHandle } }>>(new Map);
|
||||
const [ctx, setCtx] = createSignal<any>();
|
||||
|
||||
// Since the files are stored in indexedDb we need to refetch on the client in order to populate on page load
|
||||
onMount(() => {
|
||||
|
@ -57,37 +45,22 @@ export default function Edit(props) {
|
|||
if (root.state === 'ready' && directory?.kind === 'directory') {
|
||||
const contents = await Array.fromAsync(walk(directory));
|
||||
|
||||
const entries = Object.entries(
|
||||
Object.groupBy(contents, e => e.lang)
|
||||
).map(([lang, entries]) => ({
|
||||
lang,
|
||||
entries: entries!
|
||||
.map(e => e.entries)
|
||||
.reduce((o, e) => {
|
||||
Object.assign(o, e);
|
||||
const merged = contents.reduce((aggregate, { handle, path, lang, entries }) => {
|
||||
for (const [key, value] of entries.entries()) {
|
||||
if (!aggregate.has(key)) {
|
||||
aggregate.set(key, {});
|
||||
}
|
||||
|
||||
return o;
|
||||
}, {})
|
||||
}));
|
||||
|
||||
const assign = (lang: string, entries: Entry) => {
|
||||
return Object.entries(entries).reduce((aggregate, [key, value]) => {
|
||||
const v = typeof value === 'string' ? { [lang]: value } : assign(lang, value);
|
||||
|
||||
Object.assign(aggregate, { [key]: v });
|
||||
|
||||
return aggregate;
|
||||
}, {});
|
||||
}
|
||||
|
||||
const unified = contents.reduce((aggregate, { lang, entries }) => {
|
||||
Object.assign(aggregate, assign(lang, entries));
|
||||
aggregate.get(key)![lang] = { handle, value };
|
||||
}
|
||||
|
||||
return aggregate;
|
||||
}, {});
|
||||
}, new Map<string, { [lang: string]: { value: string, handle: FileSystemFileHandle } }>());
|
||||
|
||||
console.log(contents, merged);
|
||||
|
||||
setColumns(['key', ...new Set(contents.map(c => c.lang))]);
|
||||
setRows(unified);
|
||||
setRows(merged);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -124,42 +97,12 @@ export default function Edit(props) {
|
|||
}, { key: 's', modifier: Modifier.Control | Modifier.Shift }),
|
||||
edit: createCommand(() => {
|
||||
}),
|
||||
selection: createCommand(() => { }),
|
||||
selectAll: createCommand(() => {
|
||||
console.log(ctx()?.selectAll(true));
|
||||
}, { key: 'a', modifier: Modifier.Control }),
|
||||
view: createCommand(() => { }),
|
||||
} as const;
|
||||
|
||||
const [columns, setColumns] = createSignal([]);
|
||||
const [rows, setRows] = createSignal<Entry2>({});
|
||||
|
||||
const Row: Component<{ entry: Entry2 }> = (props) => {
|
||||
return <For each={Object.entries(props.entry)}>{
|
||||
([key, value]) => {
|
||||
const values = Object.values(value);
|
||||
const isLeaf = values.some(v => typeof v === 'string');
|
||||
|
||||
return <Show when={isLeaf} fallback={<Group key={key} entry={value as Entry2} />}>
|
||||
<input type="checkbox" />
|
||||
|
||||
<span>{key}</span>
|
||||
|
||||
<For each={values}>{
|
||||
value => <input type="" value={value} />
|
||||
}</For>
|
||||
</Show>;
|
||||
}
|
||||
}</For>
|
||||
};
|
||||
|
||||
const Group: Component<{ key: string, entry: Entry2 }> = (props) => {
|
||||
return <details open>
|
||||
<summary>{props.key}</summary>
|
||||
|
||||
<Row entry={props.entry} />
|
||||
</details>;
|
||||
};
|
||||
|
||||
const columnCount = createMemo(() => columns().length - 1);
|
||||
|
||||
return <>
|
||||
<Menu.Root>
|
||||
<Menu.Item label="file">
|
||||
|
@ -174,29 +117,13 @@ export default function Edit(props) {
|
|||
|
||||
<Menu.Item label="edit" command={commands.edit} />
|
||||
|
||||
<Menu.Item label="selection" command={commands.selection} />
|
||||
<Menu.Item label="selection">
|
||||
<Menu.Item label="select all" command={commands.selectAll} />
|
||||
</Menu.Item>
|
||||
|
||||
<Menu.Item label="view" command={commands.view} />
|
||||
</Menu.Root>
|
||||
|
||||
<section class="table" style={{ '--columns': columnCount() }}>
|
||||
<header>
|
||||
<input type="checkbox" />
|
||||
|
||||
<For each={columns()}>{
|
||||
column => <span>{column}</span>
|
||||
}</For>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<Row entry={rows()} />
|
||||
</main>
|
||||
</section>
|
||||
|
||||
{/* <AgGridSolid
|
||||
singleClickEdit
|
||||
columnDefs={columnDefs()}
|
||||
rowData={rowData()}
|
||||
defaultColDef={defaultColDef} /> */}
|
||||
<Grid columns={columns()} rows={rows()} context={setCtx} />
|
||||
</>
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue