diff --git a/bun.lockb b/bun.lockb index 5791086..e30b90b 100644 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index d0e27d8..62d789e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "iterator-helpers-polyfill": "^3.0.1", "solid-icons": "^1.1.0", "solid-js": "^1.9.2", + "ts-pattern": "^5.5.0", "vinxi": "^0.4.3" }, "engines": { diff --git a/src/features/file/grid.tsx b/src/features/file/grid.tsx index 2ae62da..cd86102 100644 --- a/src/features/file/grid.tsx +++ b/src/features/file/grid.tsx @@ -1,5 +1,5 @@ import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, onMount, ParentComponent, Show, useContext } from "solid-js"; -import { createStore } from "solid-js/store"; +import { createStore, unwrap } from "solid-js/store"; import { SelectionProvider, useSelection, selectable } from "../selectable"; import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities"; import css from './grid.module.css'; @@ -12,12 +12,14 @@ export interface Entry extends Record { } type Rows = Map>; export interface GridContextType { + readonly rows: Accessor>>; readonly mutations: Accessor; readonly selection: Accessor; mutate(prop: string, lang: string, value: string): void; } export interface GridApi { + readonly rows: Accessor>>; readonly mutations: Accessor; selectAll(): void; clear(): void; @@ -37,6 +39,7 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => { }); const mutations = createMemo(() => deepDiff(state.snapshot, state.rows).toArray()); + const rows = createMemo(() => Object.fromEntries(Object.entries(state.rows).map(([key, row]) => [key, unwrap(row)] as const))); createEffect(() => { setState('rows', Object.fromEntries(deepCopy(props.rows).entries())); @@ -48,6 +51,7 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => { }); const ctx: GridContextType = { + rows, mutations, selection, @@ -105,6 +109,7 @@ const Api: Component<{ api: undefined | ((api: GridApi) => any) }> = (props) => const selectionContext = useSelection(); const api: GridApi = { + rows: gridContext.rows, mutations: gridContext.mutations, selectAll() { selectionContext.selectAll(); diff --git a/src/routes/(editor)/edit.tsx b/src/routes/(editor)/edit.tsx index e024112..c06189d 100644 --- a/src/routes/(editor)/edit.tsx +++ b/src/routes/(editor)/edit.tsx @@ -1,12 +1,13 @@ -import { Menu } from "~/features/menu"; +import { children, createEffect, createMemo, createResource, createSignal, onMount, ParentProps } from "solid-js"; +import { MutarionKind, splitAt } from "~/utilities"; import { Sidebar } from "~/components/sidebar"; -import { Component, createEffect, createMemo, createResource, createSignal, onMount, ParentComponent, ParentProps } from "solid-js"; +import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree, FileEntry, Entry } from "~/components/filetree"; +import { Menu } from "~/features/menu"; import { Grid, load, useFiles } from "~/features/file"; import { Command, Context, createCommand, Modifier, noop } from "~/features/command"; import { GridApi } from "~/features/file/grid"; -import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree, FileEntry } from "~/components/filetree"; import css from "./edit.module.css"; -import { splitAt } from "~/utilities"; +import { match } from "ts-pattern"; async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): AsyncGenerator<{ id: string, handle: FileSystemFileHandle, path: string[], lang: string, entries: Map }, void, never> { for await (const handle of directory.values()) { @@ -31,13 +32,31 @@ async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): } }; +function* breadthFirstTraverse(subject: FolderEntry): Generator<{ path: string[] } & Entry, void, unknown> { + const queue: ({ path: string[] } & Entry)[] = subject.entries.map(e => ({ path: [], ...e })); + + while (queue.length > 0) { + const entry = queue.shift()!; + + yield entry; + + if (entry.kind === 'folder') { + queue.push(...entry.entries.map(e => ({ path: [...entry.path, entry.name], ...e }))); + } + } +} + +const findFile = (folder: FolderEntry, id: string) => { + return breadthFirstTraverse(folder).find((entry): entry is { path: string[] } & FileEntry => entry.kind === 'file' && entry.id === id); +} + export default function Edit(props: ParentProps) { const filesContext = useFiles(); const [root, { mutate, refetch }] = createResource(() => filesContext?.get('root')); const [tree, setFiles] = createSignal(emptyFolder); const [columns, setColumns] = createSignal([]); const [rows, setRows] = createSignal>>(new Map); - const [entries, setEntries] = createSignal>>(new Map); + const [entries, setEntries] = createSignal>>(new Map); const [api, setApi] = createSignal(); const mutatedFiles = createMemo(() => { @@ -50,7 +69,7 @@ export default function Edit(props: ParentProps) { return files.get(key)?.[lang]?.id; }) - .filter(Boolean) + .filter(file => file !== undefined) ); }); @@ -88,10 +107,6 @@ export default function Edit(props: ParentProps) { } }); - createEffect(() => { - mutatedFiles() - }); - const commands = { open: createCommand('open', async () => { const [fileHandle] = await window.showOpenFilePicker({ @@ -118,7 +133,81 @@ export default function Edit(props: ParentProps) { mutate(directory); }), save: createCommand('save', () => { - console.log('save'); + const mutations = api()?.mutations() ?? []; + + if (mutations.length === 0) { + return; + } + + const rows = api()?.rows() ?? {}; + const _entries = entries(); + + // Cases we can encounter: + // | | file extsis | no existing file | + // |---------|---------------------------|----------------------! + // | created | insert new key into file | create new file | + // | updated | update value | create new file (*1) | + // | deleted | remove key from file (*3) | no-op/skip (*2)(*3) | + // + // 1) This can happen if the key already exists in another language (so when adding a new language for example). + // 2) The same as with 1, when you delete a key, and there are not files for each language, then this is a valid case. + // 3) When a file has 0 keys, we can remove it. + + for (const mutation of mutations) { + const [key, lang] = splitAt(mutation.key, mutation.key.lastIndexOf('.')); + const entry = _entries.get(key); + const localEntry = entry?.[lang]; + + console.log(entry, localEntry); + + // TODO :: this is not really a matrix, we should resolve the file when one does not exist + // + // happy path :: When we do have both an entry and localEntry and the localEntry has an id and that file is found + + // | | entry | localEntry | id | file | + // |---|-------!------------|----!------! + // | 1 | x | x | x | x | + // | 2 | x | x | x | | + // | 3 | x | x | | | + // | 4 | x | | | | + // | 5 | | | | | + + if (!localEntry) { + throw new Error('invalid edge case???'); + } + + const file = findFile(tree(), localEntry.id); + const fileExists = file !== undefined; + + console.log(key, file?.path.join('.')); + + const fileLocalKey = key.slice(file?.path.join('.')); + + const result = match([fileExists, mutation.kind]) + .with([true, MutarionKind.Create], () => ({ action: MutarionKind.Create, key, value: rows[key][lang], file: file?.meta })) + .with([false, MutarionKind.Create], () => '2') + .with([true, MutarionKind.Update], () => ({ action: MutarionKind.Update, key, value: rows[key][lang], file: file?.meta })) + .with([false, MutarionKind.Update], () => '4') + .with([true, MutarionKind.Delete], () => ({ action: MutarionKind.Delete, key, file: file?.meta })) + .with([false, MutarionKind.Delete], () => '6') + .exhaustive(); + + console.log(mutation, key, lang, entry, file, result); + } + + // for (const fileId of files) { + // const { path, meta } = findFile(tree(), fileId) ?? {}; + + // console.log(fileId, path, meta, entries()); + + // // TODO + // // - find file handle + // // - prepare data + // // -- clone entries map (so that order is preserved) + // // -- apply mutations + // // -- convert key to file local (ergo, remove the directory path prefix) + // // - write data to file + // } }, { key: 's', modifier: Modifier.Control }), saveAs: createCommand('save as', (handle?: FileSystemFileHandle) => { console.log('save as ...', handle); diff --git a/src/utilities.ts b/src/utilities.ts index 697fbc0..d0e988d 100644 --- a/src/utilities.ts +++ b/src/utilities.ts @@ -41,10 +41,15 @@ export const deepCopy = (original: T): T => { ) as T; } -type Added = { kind: 'added', value: any }; -type Updated = { kind: 'updated', value: any, original: any }; -type Removed = { kind: 'removed' }; -export type Mutation = { key: string } & (Added | Updated | Removed); +export enum MutarionKind { + Create = 'created', + 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 function* deepDiff(a: T1, b: T2, path: string[] = []): Generator { if (!isIterable(a) || !isIterable(b)) { @@ -59,14 +64,14 @@ export function* deepDiff(a: T1, b: T2, pa } if (!keyA && keyB) { - yield { key: path.concat(keyB.toString()).join('.'), kind: 'added', value: valueB }; + yield { key: path.concat(keyB.toString()).join('.'), kind: MutarionKind.Create, value: valueB }; continue; } if (keyA && !keyB) { // value was added - yield { key: path.concat(keyA.toString()).join('.'), kind: 'removed' }; + yield { key: path.concat(keyA.toString()).join('.'), kind: MutarionKind.Delete }; continue; } @@ -84,10 +89,10 @@ export function* deepDiff(a: T1, b: T2, pa const key = path.concat(keyA!.toString()).join('.'); yield ((): Mutation => { - if (valueA === null || valueA === undefined) return { key, kind: 'added', value: valueB }; - if (valueB === null || valueB === undefined) return { key, kind: 'removed' }; + if (valueA === null || valueA === undefined) return { key, kind: MutarionKind.Create, value: valueB }; + if (valueB === null || valueB === undefined) return { key, kind: MutarionKind.Delete }; - return { key, kind: 'updated', value: valueB, original: valueA }; + return { key, kind: MutarionKind.Update, value: valueB, original: valueA }; })(); } };