implemented feature
TODOs: - extract logic to feature file to simplify component - add unit test - add end-to-end tests
This commit is contained in:
parent
316e3158db
commit
4e98849e07
3 changed files with 101 additions and 20 deletions
|
@ -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(() => {
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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)) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue