diff --git a/src/features/file/grid.tsx b/src/features/file/grid.tsx index 7baf4f0..d3c381c 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -19,6 +19,7 @@ export interface GridContextType { mutate(prop: string, lang: string, value: string): void; remove(props: string[]): void; insert(prop: string): void; + addColumn(name: string): void; } export interface GridApi { @@ -29,6 +30,7 @@ export interface GridApi { clear(): void; remove(keys: string[]): void; insert(prop: string): void; + addColumn(name: string): void; } const GridContext = createContext(); @@ -38,8 +40,9 @@ const useGrid = () => useContext(GridContext)!; export const Grid: Component<{ class?: string, columns: string[], rows: Rows, api?: (api: GridApi) => any }> = (props) => { const [selection, setSelection] = createSignal([]); - const [state, setState] = createStore<{ rows: Record>, snapshot: Rows, numberOfRows: number }>({ + const [state, setState] = createStore<{ rows: Record>, columns: string[], snapshot: Rows, numberOfRows: number }>({ rows: {}, + columns: [], snapshot: new Map, numberOfRows: 0, }); @@ -51,12 +54,17 @@ 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); createEffect(() => { setState('rows', Object.fromEntries(deepCopy(props.rows).entries())); setState('snapshot', props.rows); }); + createEffect(() => { + setState('columns', [...props.columns]); + }); + createEffect(() => { setState('numberOfRows', Object.keys(state.rows).length); }); @@ -82,18 +90,27 @@ export const Grid: Component<{ class?: string, columns: string[], rows: Rows, ap insert(prop: string) { setState('rows', produce(rows => { - rows[prop] = Object.fromEntries(props.columns.slice(1).map(lang => [lang, ''])); + rows[prop] = Object.fromEntries(state.columns.slice(1).map(lang => [lang, ''])); return rows })) }, + + addColumn(name: string): void { + setState(produce(state => { + state.columns.push(name); + state.rows = Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, { ...row, [name]: '' }])); + + return state; + })) + }, }; return - <_Grid class={props.class} columns={props.columns} rows={rows()} /> + <_Grid class={props.class} columns={columns()} rows={rows()} /> ; }; @@ -154,6 +171,10 @@ const Api: Component<{ api: undefined | ((api: GridApi) => any) }> = (props) => insert(prop: string) { gridContext.insert(prop); }, + + addColumn(name: string): void { + gridContext.addColumn(name); + }, }; createEffect(() => { diff --git a/src/routes/(editor)/edit.tsx b/src/routes/(editor)/edit.tsx index 37f7fc7..9d50b1e 100644 --- a/src/routes/(editor)/edit.tsx +++ b/src/routes/(editor)/edit.tsx @@ -1,5 +1,5 @@ import { Component, createEffect, createMemo, createSignal, For, onMount, ParentProps, Setter, Show } from "solid-js"; -import { filter, MutarionKind, Mutation, splitAt } from "~/utilities"; +import { Created, filter, MutarionKind, Mutation, splitAt } from "~/utilities"; import { Sidebar } from "~/components/sidebar"; import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree } from "~/components/filetree"; import { Menu } from "~/features/menu"; @@ -91,7 +91,8 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { const [active, setActive] = createSignal(); const [contents, setContents] = createSignal>>(new Map()); const [tree, setFiles] = createSignal(emptyFolder); - const [prompt, setPrompt] = createSignal(); + const [newKeyPrompt, setNewKeyPrompt] = createSignal(); + const [newLanguagePrompt, setNewLanguagePrompt] = createSignal(); const tab = createMemo(() => { const name = active(); @@ -99,7 +100,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { return tabs().find(t => t.handle.name === name); }); const api = createMemo(() => tab()?.api()); - const mutations = createMemo<(Mutation & { file?: { value: string, handle: FileSystemFileHandle, id: string } })[]>(() => tabs().flatMap(tab => { + const mutations = createMemo<(Mutation & { lang: string, file?: { value: string, handle: FileSystemFileHandle, id: string } })[]>(() => tabs().flatMap(tab => { const entries = tab.entries(); const files = tab.files(); const mutations = tab.api()?.mutations() ?? []; @@ -109,11 +110,19 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { case MutarionKind.Update: { const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.')); - return { kind: MutarionKind.Update, key, file: entries.get(key)?.[lang] }; + return { kind: MutarionKind.Update, key, lang, file: entries.get(key)?.[lang] }; } case MutarionKind.Create: { - return Object.entries(m.value).map(([lang, value]) => ({ kind: MutarionKind.Create, key: m.key, file: files.get(lang)!, value })); + if (typeof m.value === 'object') { + return Object.entries(m.value).map(([lang, value]) => { + return ({ kind: MutarionKind.Create, key: m.key, lang, file: files.get(lang)!, value }); + }); + } + + const [key, lang] = splitAt(m.key, m.key.lastIndexOf('.')); + + return { kind: MutarionKind.Create, key, lang, file: undefined, value: m.value }; } case MutarionKind.Delete: { @@ -137,8 +146,35 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { } const groupedByFileId = Object.groupBy(muts, m => m.file?.id ?? 'undefined'); + const newFiles = Object.entries(Object.groupBy((groupedByFileId['undefined'] ?? []) as (Created & { lang: string, file: undefined })[], m => m.lang)).map(([lang, mutations]) => { + const data = mutations!.reduce((aggregate, { key, value }) => { + let obj = aggregate; + const i = key.lastIndexOf('.'); - return entries.map(({ id, handle }) => { + if (i !== -1) { + const [k, lastPart] = splitAt(key, i); + + for (const part of k.split('.')) { + if (!Object.hasOwn(obj, part)) { + obj[part] = {}; + } + + obj = obj[part]; + } + + obj[lastPart] = value; + } + else { + obj[key] = value; + } + + return aggregate; + }, {} as Record); + + return [{ existing: false, name: lang }, data] as const; + }) + + const existingFiles = entries.map(({ id, handle }) => { const existing = new Map(files.get(id)!); const mutations = groupedByFileId[id]!; @@ -158,7 +194,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { } return [ - handle, + { existing: true, handle }, existing.entries().reduce((aggregate, [key, value]) => { let obj = aggregate; const i = key.lastIndexOf('.'); @@ -183,9 +219,15 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { return aggregate; }, {} as Record) ] as const; - }).toArray(); + }).toArray() as (readonly [({ existing: true, handle: FileSystemFileHandle } | { existing: false, name: string }), Record])[]; + + return existingFiles.concat(newFiles); }); + // createEffect(() => { + // console.log(mutatedData()); + // }); + createEffect(() => { const directory = props.root; @@ -208,7 +250,10 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { filesContext.remove(id); }, { key: 'w', modifier: Modifier.Control | (isInstalledPWA ? Modifier.None : Modifier.Alt) }), save: createCommand('save', async () => { - await Promise.allSettled(mutatedData().map(async ([handle, data]) => { + await Promise.allSettled(mutatedData().map(async ([file, data]) => { + // TODO :: add the newly created file to the known files list to that the save file picker is not shown again on subsequent saves + const handle = file.existing ? file.handle : await window.showSaveFilePicker({ suggestedName: file.name, excludeAcceptAllOption: true, types: [{ description: 'JSON file', accept: { 'application/json': ['.json'] } }] }); + const stream = await handle.createWritable({ keepExistingData: false }); stream.write(JSON.stringify(data, null, 4)); @@ -246,7 +291,7 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { remove(Object.keys(selection())); }, { key: 'delete', modifier: Modifier.None }), inserNewKey: createCommand('insert new key', async () => { - const formData = await prompt()?.showModal(); + const formData = await newKeyPrompt()?.showModal(); const key = formData?.get('key')?.toString(); if (!key) { @@ -255,7 +300,18 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { api()?.insert(key); }), - inserNewLanguage: noop.withLabel('insert new language'), + inserNewLanguage: createCommand('insert new language', async () => { + const formData = await newLanguagePrompt()?.showModal(); + const language = formData?.get('locale')?.toString(); + + if (!language) { + return; + } + + console.log(language); + + api()?.addColumn(language); + }), } as const; return
@@ -295,8 +351,12 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => { - hint: use . to denote nested keys,
i.e. this.is.some.key would be a key that is four levels deep}> - + hint: use . to denote nested keys,
i.e. this.is.some.key would be a key that is four levels deep}> + +
+ + + diff --git a/src/utilities.ts b/src/utilities.ts index 8408a4e..c47a58b 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -54,10 +54,10 @@ export enum MutarionKind { Update = 'updated', Delete = 'deleted', } -type Created = { kind: MutarionKind.Create, value: any }; -type Updated = { kind: MutarionKind.Update, value: any, original: any }; -type Deleted = { kind: MutarionKind.Delete }; -export type Mutation = { key: string } & (Created | Updated | Deleted); +export type Created = { kind: MutarionKind.Create, key: string, value: any }; +export type Updated = { kind: MutarionKind.Update, key: string, value: any, original: any }; +export type Deleted = { kind: MutarionKind.Delete, key: string }; +export type Mutation = Created | Updated | Deleted; export function* deepDiff(a: T1, b: T2, path: string[] = []): Generator { if (!isIterable(a) || !isIterable(b)) {