implemented feature

TODOs:
- extract logic to feature file to simplify component
- add unit test
- add end-to-end tests
This commit is contained in:
Chris Kruining 2024-12-02 16:26:00 +01:00
parent 316e3158db
commit 4e98849e07
No known key found for this signature in database
GPG key ID: EB894A3560CCCAD2
3 changed files with 101 additions and 20 deletions

View file

@ -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<GridContextType>();
@ -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<SelectionItem[]>([]);
const [state, setState] = createStore<{ rows: Record<string, Record<string, string>>, snapshot: Rows, numberOfRows: number }>({
const [state, setState] = createStore<{ rows: Record<string, Record<string, string>>, 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 <GridContext.Provider value={ctx}>
<SelectionProvider selection={setSelection} multiSelect>
<Api api={props.api} />
<_Grid class={props.class} columns={props.columns} rows={rows()} />
<_Grid class={props.class} columns={columns()} rows={rows()} />
</SelectionProvider>
</GridContext.Provider>;
};
@ -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(() => {

View file

@ -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<string>();
const [contents, setContents] = createSignal<Map<string, Map<string, string>>>(new Map());
const [tree, setFiles] = createSignal<FolderEntry>(emptyFolder);
const [prompt, setPrompt] = createSignal<PromptApi>();
const [newKeyPrompt, setNewKeyPrompt] = createSignal<PromptApi>();
const [newLanguagePrompt, setNewLanguagePrompt] = createSignal<PromptApi>();
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<string, any>);
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<string, any>)
] as const;
}).toArray();
}).toArray() as (readonly [({ existing: true, handle: FileSystemFileHandle } | { existing: false, name: string }), Record<string, any>])[];
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 <div class={css.root}>
@ -295,8 +351,12 @@ const Editor: Component<{ root: FileSystemDirectoryHandle }> = (props) => {
<Menu.Item command={noop.withLabel('view')} />
</Menu.Root>
<Prompt api={setPrompt} title="Which key do you want to create?" description={<>hint: use <code>.</code> to denote nested keys,<br /> i.e. <code>this.is.some.key</code> would be a key that is four levels deep</>}>
<input name="key" value="this.is.an.awesome.key" placeholder="name of new key ()" />
<Prompt api={setNewKeyPrompt} title="Which key do you want to create?" description={<>hint: use <code>.</code> to denote nested keys,<br /> i.e. <code>this.is.some.key</code> would be a key that is four levels deep</>}>
<input name="key" placeholder="name of new key ()" value="keyF.some.deeper.nested.value" />
</Prompt>
<Prompt api={setNewLanguagePrompt}>
<input name="locale" placeholder="locale code, i.e. en-GB" value="fr-FR" />
</Prompt>
<Sidebar as="aside" label={tree().name} class={css.sidebar}>

View file

@ -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<T1 extends object, T2 extends object>(a: T1, b: T2, path: string[] = []): Generator<Mutation, void, unknown> {
if (!isIterable(a) || !isIterable(b)) {