refactoring stuff to make sure the right responsibility is fullfilled in the right place
This commit is contained in:
parent
fa9264326b
commit
ddf4519f41
3 changed files with 138 additions and 127 deletions
|
@ -19,19 +19,23 @@ const useTabs = () => {
|
||||||
|
|
||||||
export const Tabs: ParentComponent<{ active?: Setter<string | undefined> }> = (props) => {
|
export const Tabs: ParentComponent<{ active?: Setter<string | undefined> }> = (props) => {
|
||||||
const [active, setActive] = createSignal<string | undefined>(undefined);
|
const [active, setActive] = createSignal<string | undefined>(undefined);
|
||||||
const [tabs, setTabs] = createSignal<{ id: string, label: string }[]>([]);
|
const [tabs, setTabs] = createSignal<Map<string, string>>(new Map());
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
props.active?.(active());
|
props.active?.(active());
|
||||||
});
|
});
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
setActive(tabs().at(-1)?.id);
|
setActive(tabs().keys().toArray().at(-1));
|
||||||
});
|
});
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
register(id: string, label: string) {
|
register(id: string, label: string) {
|
||||||
setTabs(tabs => [...tabs, { id, label }]);
|
setTabs(tabs => {
|
||||||
|
tabs.set(id, label);
|
||||||
|
|
||||||
|
return new Map(tabs);
|
||||||
|
});
|
||||||
|
|
||||||
return createMemo(() => active() === id);
|
return createMemo(() => active() === id);
|
||||||
},
|
},
|
||||||
|
@ -40,8 +44,8 @@ export const Tabs: ParentComponent<{ active?: Setter<string | undefined> }> = (p
|
||||||
return <TabsContext.Provider value={ctx}>
|
return <TabsContext.Provider value={ctx}>
|
||||||
<div class={css.tabs}>
|
<div class={css.tabs}>
|
||||||
<header>
|
<header>
|
||||||
<For each={tabs()}>{
|
<For each={tabs().entries().toArray()}>{
|
||||||
tab => <button onpointerdown={() => setActive(tab.id)} classList={{ [css.active]: active() === tab.id }}>{tab.label}</button>
|
([id, label]) => <button onpointerdown={() => setActive(id)} classList={{ [css.active]: active() === id }}>{label}</button>
|
||||||
}</For>
|
}</For>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
|
@ -1,38 +1,68 @@
|
||||||
import Dexie, { EntityTable } from "dexie";
|
import Dexie, { EntityTable } from "dexie";
|
||||||
import { createContext, ParentComponent, useContext } from "solid-js";
|
import { Accessor, createContext, createMemo, onMount, ParentComponent, useContext } from "solid-js";
|
||||||
|
import { createStore } from "solid-js/store";
|
||||||
import { isServer } from "solid-js/web";
|
import { isServer } from "solid-js/web";
|
||||||
import * as json from './parser/json';
|
import * as json from './parser/json';
|
||||||
|
|
||||||
interface FileEntity {
|
interface FileEntity {
|
||||||
name: string;
|
key: string;
|
||||||
handle: FileSystemDirectoryHandle;
|
handle: FileSystemDirectoryHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Store = Dexie & {
|
type Store = Dexie & {
|
||||||
files: EntityTable<FileEntity, 'name'>;
|
files: EntityTable<FileEntity, 'key'>;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface FilesContextType {
|
interface InternalFilesContextType {
|
||||||
set(name: string, handle: FileSystemDirectoryHandle): Promise<void>;
|
onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any): void;
|
||||||
get(name: string): Promise<FileSystemDirectoryHandle | undefined>;
|
set(key: string, handle: FileSystemDirectoryHandle): Promise<void>;
|
||||||
|
get(key: string): Promise<FileSystemDirectoryHandle | undefined>;
|
||||||
|
remove(key: string): Promise<void>;
|
||||||
|
keys(): Promise<string[]>;
|
||||||
|
entries(): Promise<FileEntity[]>;
|
||||||
list(): Promise<FileSystemDirectoryHandle[]>;
|
list(): Promise<FileSystemDirectoryHandle[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface FilesContextType {
|
||||||
|
readonly files: Accessor<FileEntity[]>,
|
||||||
|
|
||||||
|
get(key: string): Accessor<FileSystemDirectoryHandle | undefined>
|
||||||
|
set(key: string, handle: FileSystemDirectoryHandle): Promise<void>;
|
||||||
|
remove(key: string): Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
const FilesContext = createContext<FilesContextType>();
|
const FilesContext = createContext<FilesContextType>();
|
||||||
|
|
||||||
const clientContext = (): FilesContextType => {
|
const clientContext = (): InternalFilesContextType => {
|
||||||
const db = new Dexie('Files') as Store;
|
const db = new Dexie('Files') as Store;
|
||||||
|
|
||||||
db.version(1).stores({
|
db.version(1).stores({
|
||||||
files: 'name, handle'
|
files: 'key, handle'
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
async set(name: string, handle: FileSystemDirectoryHandle) {
|
onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any) {
|
||||||
await db.files.put({ name, handle });
|
const callHook = (key: string, handle: FileSystemDirectoryHandle) => setTimeout(() => hook(key, handle), 1);
|
||||||
|
|
||||||
|
db.files.hook('creating', (_: string, { key, handle }: FileEntity) => { callHook(key, handle); });
|
||||||
|
db.files.hook('deleting', (_: string, { key, handle }: FileEntity) => callHook(key, handle));
|
||||||
|
db.files.hook('updating', (_1: Object, _2: string, { key, handle }: FileEntity) => callHook(key, handle));
|
||||||
},
|
},
|
||||||
async get(name: string) {
|
|
||||||
return (await db.files.get(name))?.handle;
|
async set(key: string, handle: FileSystemDirectoryHandle) {
|
||||||
|
await db.files.put({ key, handle });
|
||||||
|
},
|
||||||
|
async get(key: string) {
|
||||||
|
return (await db.files.get(key))?.handle;
|
||||||
|
},
|
||||||
|
async remove(key: string) {
|
||||||
|
return (await db.files.delete(key));
|
||||||
|
},
|
||||||
|
async keys() {
|
||||||
|
return (await db.files.toArray()).map(f => f.key);
|
||||||
|
},
|
||||||
|
async entries() {
|
||||||
|
return await db.files.toArray();
|
||||||
},
|
},
|
||||||
async list() {
|
async list() {
|
||||||
const files = await db.files.toArray();
|
const files = await db.files.toArray();
|
||||||
|
@ -42,25 +72,69 @@ const clientContext = (): FilesContextType => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const serverContext = (): FilesContextType => ({
|
const serverContext = (): InternalFilesContextType => ({
|
||||||
set() {
|
onChange(hook: (key: string, handle: FileSystemDirectoryHandle) => any) {
|
||||||
|
|
||||||
|
},
|
||||||
|
set(key: string, handle: FileSystemDirectoryHandle) {
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
},
|
},
|
||||||
get(name: string) {
|
get(key: string) {
|
||||||
return Promise.resolve(undefined);
|
return Promise.resolve(undefined);
|
||||||
},
|
},
|
||||||
|
remove(key: string) {
|
||||||
|
return Promise.resolve(undefined);
|
||||||
|
},
|
||||||
|
keys() {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
|
entries() {
|
||||||
|
return Promise.resolve([]);
|
||||||
|
},
|
||||||
list() {
|
list() {
|
||||||
return Promise.resolve<FileSystemDirectoryHandle[]>([]);
|
return Promise.resolve([]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const FilesProvider: ParentComponent = (props) => {
|
export const FilesProvider: ParentComponent = (props) => {
|
||||||
const ctx = isServer ? serverContext() : clientContext();
|
const internal = isServer ? serverContext() : clientContext();
|
||||||
|
|
||||||
return <FilesContext.Provider value={ctx}>{props.children}</FilesContext.Provider>;
|
const [state, setState] = createStore<{ files: FileEntity[] }>({ files: [] });
|
||||||
|
|
||||||
|
const updateFilesInState = async () => {
|
||||||
|
const entities = await internal.entries();
|
||||||
|
|
||||||
|
setState('files', entities);
|
||||||
|
};
|
||||||
|
|
||||||
|
internal.onChange((key: string, handle: FileSystemDirectoryHandle) => {
|
||||||
|
updateFilesInState();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
updateFilesInState();
|
||||||
|
});
|
||||||
|
|
||||||
|
const context: FilesContextType = {
|
||||||
|
files: createMemo(() => state.files),
|
||||||
|
|
||||||
|
get(key: string): Accessor<FileSystemDirectoryHandle | undefined> {
|
||||||
|
return createMemo(() => state.files.find(entity => entity.key === key)?.handle);
|
||||||
|
},
|
||||||
|
|
||||||
|
async set(key: string, handle: FileSystemDirectoryHandle) {
|
||||||
|
await internal.set(key, handle);
|
||||||
|
},
|
||||||
|
|
||||||
|
async remove(key: string) {
|
||||||
|
await internal.remove(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return <FilesContext.Provider value={context}>{props.children}</FilesContext.Provider>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useFiles = () => useContext(FilesContext);
|
export const useFiles = () => useContext(FilesContext)!;
|
||||||
|
|
||||||
export const load = (file: File): Promise<Map<string, string> | undefined> => {
|
export const load = (file: File): Promise<Map<string, string> | undefined> => {
|
||||||
switch (file.type) {
|
switch (file.type) {
|
||||||
|
|
|
@ -62,18 +62,14 @@ interface ContentTabType {
|
||||||
|
|
||||||
export default function Edit(props: ParentProps) {
|
export default function Edit(props: ParentProps) {
|
||||||
const filesContext = useFiles();
|
const filesContext = useFiles();
|
||||||
const [root, { refetch: getRoot, mutate: updateRoot }] = createResource(() => filesContext?.get('__root__'));
|
|
||||||
const [tabs, { refetch: getTabs }] = createResource<ContentTabType[]>(async () => {
|
|
||||||
const handles = (await filesContext?.list()) ?? [];
|
|
||||||
|
|
||||||
return await Promise.all(handles.map(handle => {
|
const root = filesContext.get('__root__');
|
||||||
const [api, setApi] = createSignal<GridApi>();
|
const tabs = createMemo(() => filesContext.files().map(({ key, handle }) => {
|
||||||
const [entries, setEntries] = createSignal<Entries>(new Map());
|
const [api, setApi] = createSignal<GridApi>();
|
||||||
const files = handle.entries()
|
const [entries, setEntries] = createSignal<Entries>(new Map());
|
||||||
|
|
||||||
return ({ handle, api, setApi, entries, setEntries });
|
return ({ handle, api, setApi, entries, setEntries });
|
||||||
}));
|
}));
|
||||||
}, { initialValue: [], ssrLoadFrom: 'initial' });
|
|
||||||
const [active, setActive] = createSignal<string>();
|
const [active, setActive] = createSignal<string>();
|
||||||
const [contents, setContents] = createSignal<Map<string, Map<string, string>>>(new Map());
|
const [contents, setContents] = createSignal<Map<string, Map<string, string>>>(new Map());
|
||||||
const [tree, setFiles] = createSignal<FolderEntry>(emptyFolder);
|
const [tree, setFiles] = createSignal<FolderEntry>(emptyFolder);
|
||||||
|
@ -152,119 +148,56 @@ export default function Edit(props: ParentProps) {
|
||||||
}).toArray();
|
}).toArray();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Since the files are stored in indexedDb we need to refetch on the client in order to populate on page load
|
|
||||||
onMount(() => {
|
|
||||||
getRoot();
|
|
||||||
getTabs();
|
|
||||||
});
|
|
||||||
|
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
const directory = root();
|
const directory = root();
|
||||||
|
|
||||||
if (root.state === 'ready' && directory?.kind === 'directory') {
|
if (directory === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
const contents = await Array.fromAsync(walk(directory));
|
const contents = await Array.fromAsync(walk(directory));
|
||||||
|
|
||||||
console.log(contents);
|
setContents(new Map(contents.map(({ id, entries }) => [id, entries] as const)))
|
||||||
|
|
||||||
setContents(new Map(contents.map(({ id, entries }) => [id, entries] as const)))
|
const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]);
|
||||||
|
|
||||||
const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]);
|
const merged = contents.reduce((aggregate, { id, handle, path, lang, entries }) => {
|
||||||
|
for (const [key, value] of entries.entries()) {
|
||||||
|
const k = [...path, key].join('.');
|
||||||
|
|
||||||
const merged = contents.reduce((aggregate, { id, handle, path, lang, entries }) => {
|
if (!aggregate.has(k)) {
|
||||||
for (const [key, value] of entries.entries()) {
|
aggregate.set(k, Object.fromEntries(template));
|
||||||
const k = [...path, key].join('.');
|
|
||||||
|
|
||||||
if (!aggregate.has(k)) {
|
|
||||||
aggregate.set(k, Object.fromEntries(template));
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregate.get(k)![lang] = { value, handle, id };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return aggregate;
|
aggregate.get(k)![lang] = { value, handle, id };
|
||||||
}, new Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>());
|
}
|
||||||
|
|
||||||
setFiles({ name: directory.name, id: '', kind: 'folder', handle: directory, entries: await Array.fromAsync(fileTreeWalk(directory)) });
|
return aggregate;
|
||||||
setEntries(merged);
|
}, new Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>());
|
||||||
})();
|
|
||||||
}
|
setFiles({ name: directory.name, id: '', kind: 'folder', handle: directory, entries: await Array.fromAsync(fileTreeWalk(directory)) });
|
||||||
|
setEntries(merged);
|
||||||
|
})();
|
||||||
});
|
});
|
||||||
|
|
||||||
const commands = {
|
const commands = {
|
||||||
open: createCommand('open', async () => {
|
open: createCommand('open folder', async () => {
|
||||||
const [fileHandle] = await window.showOpenFilePicker({
|
|
||||||
types: [
|
|
||||||
{
|
|
||||||
description: "JSON File(s)",
|
|
||||||
accept: {
|
|
||||||
"application/json": [".json", ".jsonp", ".jsonc"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
excludeAcceptAllOption: true,
|
|
||||||
multiple: true,
|
|
||||||
});
|
|
||||||
const file = await fileHandle.getFile();
|
|
||||||
const text = await file.text();
|
|
||||||
|
|
||||||
console.log(fileHandle, file, text);
|
|
||||||
}, { key: 'o', modifier: Modifier.Control }),
|
|
||||||
openFolder: createCommand('open folder', async () => {
|
|
||||||
const directory = await window.showDirectoryPicker({ mode: 'readwrite' });
|
const directory = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||||||
|
|
||||||
filesContext.set('__root__', directory);
|
filesContext.set('__root__', directory);
|
||||||
updateRoot(directory);
|
}, { key: 'o', modifier: Modifier.Control }),
|
||||||
|
close: createCommand('close folder', async () => {
|
||||||
|
filesContext.remove('__root__');
|
||||||
}),
|
}),
|
||||||
save: createCommand('save', async () => {
|
save: createCommand('save', async () => {
|
||||||
const results = await Promise.allSettled(mutatedData().map(async ([handle, data]) => {
|
await Promise.allSettled(mutatedData().map(async ([handle, data]) => {
|
||||||
const stream = await handle.createWritable({ keepExistingData: false });
|
const stream = await handle.createWritable({ keepExistingData: false });
|
||||||
|
|
||||||
stream.write(JSON.stringify(data, null, 4));
|
stream.write(JSON.stringify(data, null, 4));
|
||||||
stream.write('\n');
|
stream.write('\n');
|
||||||
stream.close();
|
stream.close();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
console.log(results);
|
|
||||||
|
|
||||||
// const fileMutations = await Promise.all(mutations.map(async (mutation) => {
|
|
||||||
// const [k, lang] = splitAt(mutation.key, mutation.key.lastIndexOf('.'));
|
|
||||||
// const entry = _entries.get(k);
|
|
||||||
// const localEntry = entry?.[lang];
|
|
||||||
|
|
||||||
// if (!localEntry) {
|
|
||||||
// throw new Error('invalid edge case???');
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const createNewFile = async () => {
|
|
||||||
// const [, alternativeLocalEntry] = Object.entries(entry).find(([l, e]) => l !== lang && e.id !== undefined) ?? [];
|
|
||||||
// const { directory, path } = alternativeLocalEntry ? findFile(tree(), alternativeLocalEntry.id) ?? {} : {};
|
|
||||||
|
|
||||||
// const handle = await window.showSaveFilePicker({
|
|
||||||
// suggestedName: `${lang}.json`,
|
|
||||||
// startIn: directory,
|
|
||||||
// excludeAcceptAllOption: true,
|
|
||||||
// types: [
|
|
||||||
// { accept: { 'application/json': ['.json'] }, description: 'JSON' },
|
|
||||||
// ]
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // TODO :: patch the tree with this new entry
|
|
||||||
// // console.log(localEntry, tree());
|
|
||||||
|
|
||||||
// return { handle, path };
|
|
||||||
// };
|
|
||||||
|
|
||||||
// const { handle, path } = findFile(tree(), localEntry.id) ?? (mutation.kind !== MutarionKind.Delete ? await createNewFile() : {});
|
|
||||||
// const id = await handle?.getUniqueId();
|
|
||||||
// const key = path ? k.slice(path.join('.').length + 1) : k;
|
|
||||||
// const value = rows[k][lang];
|
|
||||||
|
|
||||||
// return { action: mutation.kind, key, id, value, handle };
|
|
||||||
// }));
|
|
||||||
|
|
||||||
// console.log(rows, entries(), Object.groupBy(fileMutations, m => m.id ?? 'undefined'))
|
|
||||||
}, { 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);
|
||||||
|
@ -301,7 +234,9 @@ export default function Edit(props: ParentProps) {
|
||||||
<Menu.Item label="file">
|
<Menu.Item label="file">
|
||||||
<Menu.Item command={commands.open} />
|
<Menu.Item command={commands.open} />
|
||||||
|
|
||||||
<Menu.Item command={commands.openFolder} />
|
<Menu.Item command={commands.close} />
|
||||||
|
|
||||||
|
<Menu.Separator />
|
||||||
|
|
||||||
<Menu.Item command={commands.save} />
|
<Menu.Item command={commands.save} />
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
@ -326,12 +261,11 @@ export default function Edit(props: ParentProps) {
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
|
|
||||||
<Sidebar as="aside" label={tree().name} class={css.sidebar}>
|
<Sidebar as="aside" label={tree().name} class={css.sidebar}>
|
||||||
<Show when={!root.loading && root()} fallback={<button onpointerdown={() => commands.openFolder()}>open a folder</button>}>
|
<Show when={root()} fallback={<button onpointerdown={() => commands.open()}>open a folder</button>}>
|
||||||
<Tree entries={tree().entries}>{[
|
<Tree entries={tree().entries}>{[
|
||||||
folder => {
|
folder => {
|
||||||
return <span onDblClick={() => {
|
return <span onDblClick={() => {
|
||||||
filesContext?.set(folder().name, folder().handle);
|
filesContext?.set(folder().name, folder().handle);
|
||||||
getTabs();
|
|
||||||
}}>{folder().name}</span>;
|
}}>{folder().name}</span>;
|
||||||
},
|
},
|
||||||
file => {
|
file => {
|
||||||
|
@ -340,7 +274,6 @@ export default function Edit(props: ParentProps) {
|
||||||
return <Context.Handle classList={{ [css.mutated]: mutated() }} onDblClick={() => {
|
return <Context.Handle classList={{ [css.mutated]: mutated() }} onDblClick={() => {
|
||||||
const folder = file().directory;
|
const folder = file().directory;
|
||||||
filesContext?.set(folder.name, folder);
|
filesContext?.set(folder.name, folder);
|
||||||
getTabs();
|
|
||||||
}}>{file().name}</Context.Handle>;
|
}}>{file().name}</Context.Handle>;
|
||||||
},
|
},
|
||||||
] as const}</Tree>
|
] as const}</Tree>
|
||||||
|
@ -349,7 +282,7 @@ export default function Edit(props: ParentProps) {
|
||||||
|
|
||||||
<Tabs active={setActive}>
|
<Tabs active={setActive}>
|
||||||
<For each={tabs()}>{
|
<For each={tabs()}>{
|
||||||
({ handle, setApi, setEntries }) => <Tab id={handle.name} label={handle.name}><Content directory={handle} api={setApi} entries={setEntries} /></Tab>
|
({ handle, setApi, setEntries }) => <Tab id={handle.name} label={handle.name} ><Content directory={handle} api={setApi} entries={setEntries} /></Tab>
|
||||||
}</For>
|
}</For>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</Context.Root>
|
</Context.Root>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue