made a start on the logic for saving files
This commit is contained in:
parent
6064fd3b45
commit
a6fc5720d4
5 changed files with 121 additions and 21 deletions
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
@ -8,6 +8,7 @@
|
||||||
"iterator-helpers-polyfill": "^3.0.1",
|
"iterator-helpers-polyfill": "^3.0.1",
|
||||||
"solid-icons": "^1.1.0",
|
"solid-icons": "^1.1.0",
|
||||||
"solid-js": "^1.9.2",
|
"solid-js": "^1.9.2",
|
||||||
|
"ts-pattern": "^5.5.0",
|
||||||
"vinxi": "^0.4.3"
|
"vinxi": "^0.4.3"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, onMount, ParentComponent, Show, useContext } from "solid-js";
|
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 { SelectionProvider, useSelection, selectable } from "../selectable";
|
||||||
import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities";
|
import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities";
|
||||||
import css from './grid.module.css';
|
import css from './grid.module.css';
|
||||||
|
@ -12,12 +12,14 @@ export interface Entry extends Record<string, Entry | Leaf> { }
|
||||||
type Rows = Map<string, Record<string, string>>;
|
type Rows = Map<string, Record<string, string>>;
|
||||||
|
|
||||||
export interface GridContextType {
|
export interface GridContextType {
|
||||||
|
readonly rows: Accessor<Record<string, Record<string, string>>>;
|
||||||
readonly mutations: Accessor<Mutation[]>;
|
readonly mutations: Accessor<Mutation[]>;
|
||||||
readonly selection: Accessor<object[]>;
|
readonly selection: Accessor<object[]>;
|
||||||
mutate(prop: string, lang: string, value: string): void;
|
mutate(prop: string, lang: string, value: string): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GridApi {
|
export interface GridApi {
|
||||||
|
readonly rows: Accessor<Record<string, Record<string, string>>>;
|
||||||
readonly mutations: Accessor<Mutation[]>;
|
readonly mutations: Accessor<Mutation[]>;
|
||||||
selectAll(): void;
|
selectAll(): void;
|
||||||
clear(): void;
|
clear(): void;
|
||||||
|
@ -37,6 +39,7 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const mutations = createMemo(() => deepDiff(state.snapshot, state.rows).toArray());
|
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(() => {
|
createEffect(() => {
|
||||||
setState('rows', Object.fromEntries(deepCopy(props.rows).entries()));
|
setState('rows', Object.fromEntries(deepCopy(props.rows).entries()));
|
||||||
|
@ -48,6 +51,7 @@ const GridProvider: ParentComponent<{ rows: Rows }> = (props) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctx: GridContextType = {
|
const ctx: GridContextType = {
|
||||||
|
rows,
|
||||||
mutations,
|
mutations,
|
||||||
selection,
|
selection,
|
||||||
|
|
||||||
|
@ -105,6 +109,7 @@ const Api: Component<{ api: undefined | ((api: GridApi) => any) }> = (props) =>
|
||||||
const selectionContext = useSelection();
|
const selectionContext = useSelection();
|
||||||
|
|
||||||
const api: GridApi = {
|
const api: GridApi = {
|
||||||
|
rows: gridContext.rows,
|
||||||
mutations: gridContext.mutations,
|
mutations: gridContext.mutations,
|
||||||
selectAll() {
|
selectAll() {
|
||||||
selectionContext.selectAll();
|
selectionContext.selectAll();
|
||||||
|
|
|
@ -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 { 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 { Grid, load, useFiles } from "~/features/file";
|
||||||
import { Command, Context, createCommand, Modifier, noop } from "~/features/command";
|
import { Command, Context, createCommand, Modifier, noop } from "~/features/command";
|
||||||
import { GridApi } from "~/features/file/grid";
|
import { GridApi } from "~/features/file/grid";
|
||||||
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree, FileEntry } from "~/components/filetree";
|
|
||||||
import css from "./edit.module.css";
|
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<string, string> }, void, never> {
|
async function* walk(directory: FileSystemDirectoryHandle, path: string[] = []): AsyncGenerator<{ id: string, handle: FileSystemFileHandle, path: string[], lang: string, entries: Map<string, string> }, void, never> {
|
||||||
for await (const handle of directory.values()) {
|
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) {
|
export default function Edit(props: ParentProps) {
|
||||||
const filesContext = useFiles();
|
const filesContext = useFiles();
|
||||||
const [root, { mutate, refetch }] = createResource(() => filesContext?.get('root'));
|
const [root, { mutate, refetch }] = createResource(() => filesContext?.get('root'));
|
||||||
const [tree, setFiles] = createSignal<FolderEntry>(emptyFolder);
|
const [tree, setFiles] = createSignal<FolderEntry>(emptyFolder);
|
||||||
const [columns, setColumns] = createSignal<string[]>([]);
|
const [columns, setColumns] = createSignal<string[]>([]);
|
||||||
const [rows, setRows] = createSignal<Map<string, Record<string, string>>>(new Map);
|
const [rows, setRows] = createSignal<Map<string, Record<string, string>>>(new Map);
|
||||||
const [entries, setEntries] = createSignal<Map<string, Record<string, { id: String, value: string, handle: FileSystemFileHandle }>>>(new Map);
|
const [entries, setEntries] = createSignal<Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>>(new Map);
|
||||||
const [api, setApi] = createSignal<GridApi>();
|
const [api, setApi] = createSignal<GridApi>();
|
||||||
|
|
||||||
const mutatedFiles = createMemo(() => {
|
const mutatedFiles = createMemo(() => {
|
||||||
|
@ -50,7 +69,7 @@ export default function Edit(props: ParentProps) {
|
||||||
|
|
||||||
return files.get(key)?.[lang]?.id;
|
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 = {
|
const commands = {
|
||||||
open: createCommand('open', async () => {
|
open: createCommand('open', async () => {
|
||||||
const [fileHandle] = await window.showOpenFilePicker({
|
const [fileHandle] = await window.showOpenFilePicker({
|
||||||
|
@ -118,7 +133,81 @@ export default function Edit(props: ParentProps) {
|
||||||
mutate(directory);
|
mutate(directory);
|
||||||
}),
|
}),
|
||||||
save: createCommand('save', () => {
|
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 }),
|
}, { key: 's', modifier: Modifier.Control }),
|
||||||
saveAs: createCommand('save as', (handle?: FileSystemFileHandle) => {
|
saveAs: createCommand('save as', (handle?: FileSystemFileHandle) => {
|
||||||
console.log('save as ...', handle);
|
console.log('save as ...', handle);
|
||||||
|
|
|
@ -41,10 +41,15 @@ export const deepCopy = <T>(original: T): T => {
|
||||||
) as T;
|
) as T;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Added = { kind: 'added', value: any };
|
export enum MutarionKind {
|
||||||
type Updated = { kind: 'updated', value: any, original: any };
|
Create = 'created',
|
||||||
type Removed = { kind: 'removed' };
|
Update = 'updated',
|
||||||
export type Mutation = { key: string } & (Added | Updated | Removed);
|
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<T1 extends object, T2 extends object>(a: T1, b: T2, path: string[] = []): Generator<Mutation, void, unknown> {
|
export function* deepDiff<T1 extends object, T2 extends object>(a: T1, b: T2, path: string[] = []): Generator<Mutation, void, unknown> {
|
||||||
if (!isIterable(a) || !isIterable(b)) {
|
if (!isIterable(a) || !isIterable(b)) {
|
||||||
|
@ -59,14 +64,14 @@ export function* deepDiff<T1 extends object, T2 extends object>(a: T1, b: T2, pa
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!keyA && keyB) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keyA && !keyB) {
|
if (keyA && !keyB) {
|
||||||
// value was added
|
// value was added
|
||||||
yield { key: path.concat(keyA.toString()).join('.'), kind: 'removed' };
|
yield { key: path.concat(keyA.toString()).join('.'), kind: MutarionKind.Delete };
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -84,10 +89,10 @@ export function* deepDiff<T1 extends object, T2 extends object>(a: T1, b: T2, pa
|
||||||
const key = path.concat(keyA!.toString()).join('.');
|
const key = path.concat(keyA!.toString()).join('.');
|
||||||
|
|
||||||
yield ((): Mutation => {
|
yield ((): Mutation => {
|
||||||
if (valueA === null || valueA === undefined) return { key, kind: 'added', value: valueB };
|
if (valueA === null || valueA === undefined) return { key, kind: MutarionKind.Create, value: valueB };
|
||||||
if (valueB === null || valueB === undefined) return { key, kind: 'removed' };
|
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 };
|
||||||
})();
|
})();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue