started introduction of splitting into tabs
This commit is contained in:
parent
1472bd8116
commit
b03f80f34f
10 changed files with 255 additions and 159 deletions
|
@ -10,21 +10,22 @@ export interface FileEntry {
|
|||
name: string;
|
||||
id: string;
|
||||
kind: 'file';
|
||||
meta: File;
|
||||
handle: FileSystemFileHandle;
|
||||
directory: FileSystemDirectoryHandle;
|
||||
meta: File;
|
||||
}
|
||||
|
||||
export interface FolderEntry {
|
||||
name: string;
|
||||
id: string;
|
||||
kind: 'folder';
|
||||
handle: FileSystemDirectoryHandle;
|
||||
entries: Entry[];
|
||||
}
|
||||
|
||||
export type Entry = FileEntry | FolderEntry;
|
||||
|
||||
export const emptyFolder: FolderEntry = { name: '', id: '', kind: 'folder', entries: [] } as const;
|
||||
export const emptyFolder: FolderEntry = { name: '', id: '', kind: 'folder', entries: [], handle: undefined as unknown as FileSystemDirectoryHandle } as const;
|
||||
|
||||
export async function* walk(directory: FileSystemDirectoryHandle, filters: RegExp[] = [], depth = 0): AsyncGenerator<Entry, void, never> {
|
||||
if (depth === 10) {
|
||||
|
@ -39,10 +40,10 @@ export async function* walk(directory: FileSystemDirectoryHandle, filters: RegEx
|
|||
const id = await handle.getUniqueId();
|
||||
|
||||
if (handle.kind === 'file') {
|
||||
yield { name: handle.name, id, kind: 'file', meta: await handle.getFile(), handle, directory };
|
||||
yield { name: handle.name, id, handle, kind: 'file', meta: await handle.getFile(), directory };
|
||||
}
|
||||
else {
|
||||
yield { name: handle.name, id, kind: 'folder', entries: await Array.fromAsync(walk(handle, filters, depth + 1)) };
|
||||
yield { name: handle.name, id, handle, kind: 'folder', entries: await Array.fromAsync(walk(handle, filters, depth + 1)) };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +54,7 @@ interface TreeContextType {
|
|||
|
||||
const TreeContext = createContext<TreeContextType>();
|
||||
|
||||
export const Tree: Component<{ entries: Entry[], children: (file: Accessor<FileEntry>) => JSX.Element, open?: TreeContextType['open'] }> = (props) => {
|
||||
export const Tree: Component<{ entries: Entry[], children: readonly [(folder: Accessor<FolderEntry>) => JSX.Element, (file: Accessor<FileEntry>) => JSX.Element], open?: TreeContextType['open'] }> = (props) => {
|
||||
const [selection, setSelection] = createSignal<object[]>([]);
|
||||
|
||||
const context = {
|
||||
|
@ -67,7 +68,7 @@ export const Tree: Component<{ entries: Entry[], children: (file: Accessor<FileE
|
|||
</SelectionProvider>;
|
||||
}
|
||||
|
||||
const _Tree: Component<{ entries: Entry[], children: (file: Accessor<FileEntry>) => JSX.Element }> = (props) => {
|
||||
const _Tree: Component<{ entries: Entry[], children: readonly [(folder: Accessor<FolderEntry>) => JSX.Element, (file: Accessor<FileEntry>) => JSX.Element] }> = (props) => {
|
||||
const context = useContext(TreeContext);
|
||||
|
||||
return <For each={props.entries.sort(sort_by('kind'))}>{
|
||||
|
@ -77,17 +78,17 @@ const _Tree: Component<{ entries: Entry[], children: (file: Accessor<FileEntry>)
|
|||
}</Show>
|
||||
|
||||
<Show when={entry.kind === 'file' ? entry : undefined}>{
|
||||
file => <span use:selectable={{ value: file() }} ondblclick={() => context?.open(file().meta)}><AiFillFile /> {props.children(file)}</span>
|
||||
file => <span use:selectable={{ value: file() }} ondblclick={() => context?.open(file().meta)}><AiFillFile /> {props.children[1](file)}</span>
|
||||
}</Show>
|
||||
</>
|
||||
}</For>
|
||||
}
|
||||
|
||||
const Folder: Component<{ folder: FolderEntry, children: (file: Accessor<FileEntry>) => JSX.Element }> = (props) => {
|
||||
const Folder: Component<{ folder: FolderEntry, children: readonly [(folder: Accessor<FolderEntry>) => JSX.Element, (file: Accessor<FileEntry>) => JSX.Element] }> = (props) => {
|
||||
const [open, setOpen] = createSignal(true);
|
||||
|
||||
return <details open={open()} ontoggle={() => debounce(() => setOpen(o => !o), 1)}>
|
||||
<summary><Show when={open()} fallback={<AiFillFolder />}><AiFillFolderOpen /></Show> {props.folder.name}</summary>
|
||||
<summary><Show when={open()} fallback={<AiFillFolder />}><AiFillFolderOpen /></Show> {props.children[0](() => props.folder)}</summary>
|
||||
<_Tree entries={props.folder.entries} children={props.children} />
|
||||
</details>;
|
||||
};
|
||||
|
|
|
@ -1,13 +1,41 @@
|
|||
.root {
|
||||
.tabs {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid: auto minmax(0, 1fr) / repeat(var(--tab-count), auto);
|
||||
grid: auto minmax(0, 1fr) / 100%;
|
||||
justify-content: start;
|
||||
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
|
||||
& > header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
border-block-end: 1px solid var(--surface-5);
|
||||
|
||||
& > button {
|
||||
background-color: var(--surface-1);
|
||||
color: var(--text-2);
|
||||
padding: var(--padding-m) var(--padding-l);
|
||||
border: none;
|
||||
|
||||
&.active {
|
||||
background-color: var(--surface-3);
|
||||
color: var(--text-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab {
|
||||
display: contents;
|
||||
position: absolute;
|
||||
grid-area: 2 / 1 / span 1 / span 1;
|
||||
inline-size: 100%;
|
||||
block-size: 100%;
|
||||
|
||||
&:not(.active) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
& > summary {
|
||||
grid-row: 1 / 1;
|
||||
|
|
|
@ -1,46 +1,83 @@
|
|||
import { Accessor, children, createContext, createMemo, createSignal, createUniqueId, For, JSX, ParentComponent, useContext } from "solid-js";
|
||||
import { Accessor, children, createContext, createEffect, createMemo, createRenderEffect, createSignal, createUniqueId, For, JSX, onMount, ParentComponent, Show, useContext } from "solid-js";
|
||||
import css from "./tabs.module.css";
|
||||
|
||||
interface TabsContextType {
|
||||
activate(id: string): void;
|
||||
active: Accessor<string | undefined>;
|
||||
isActive(id: string): Accessor<boolean>;
|
||||
register(id: string, label: string): Accessor<boolean>;
|
||||
}
|
||||
|
||||
const TabsContext = createContext<TabsContextType>();
|
||||
|
||||
const useTabs = () => {
|
||||
const context = useContext(TabsContext);
|
||||
|
||||
if (context === undefined) {
|
||||
throw new Error('<Tab /> is used outside of a <Tabs />')
|
||||
}
|
||||
|
||||
return context!;
|
||||
}
|
||||
|
||||
export const Tabs: ParentComponent = (props) => {
|
||||
const [active, setActive] = createSignal<string | undefined>(undefined);
|
||||
const numberOfTabs = createMemo(() => children(() => props.children).toArray().length);
|
||||
const [tabs, setTabs] = createSignal<{ id: string, label: string }[]>([]);
|
||||
// const resolved = children(() => props.children);
|
||||
// const resolvedArray = createMemo(() => resolved.toArray());
|
||||
// const tabs = createMemo(() => resolvedArray().map(t => ({ id: t.id, label: t.dataset?.label ?? '' })));
|
||||
|
||||
return <TabsContext.Provider value={{
|
||||
activate(id: string) {
|
||||
setActive(id);
|
||||
},
|
||||
// createEffect(() => {
|
||||
// for (const t of resolvedArray()) {
|
||||
// console.log(t);
|
||||
// }
|
||||
// });
|
||||
|
||||
active,
|
||||
createEffect(() => {
|
||||
setActive(tabs().at(-1)?.id);
|
||||
});
|
||||
|
||||
// createRenderEffect(() => {
|
||||
// if (isServer) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// for (const t of resolvedArray().filter(t => t instanceof HTMLElement)) {
|
||||
// if (active() === t.id) {
|
||||
// t.classList.add(css.active);
|
||||
// } else {
|
||||
// t.classList.remove(css.active);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
|
||||
const ctx = {
|
||||
register(id: string, label: string) {
|
||||
setTabs(tabs => [...tabs, { id, label }]);
|
||||
|
||||
isActive(id: string) {
|
||||
return createMemo(() => active() === id);
|
||||
},
|
||||
}}>
|
||||
<div class={css.root} style={{ '--tab-count': numberOfTabs() }}>
|
||||
};
|
||||
|
||||
createEffect(() => {
|
||||
console.log(tabs());
|
||||
});
|
||||
|
||||
return <TabsContext.Provider value={ctx}>
|
||||
<div class={css.tabs}>
|
||||
<header>
|
||||
<For each={tabs()}>{
|
||||
tab => <button onpointerdown={() => setActive(tab.id)} classList={{ [css.active]: active() === tab.id }}>{tab.label}</button>
|
||||
}</For>
|
||||
</header>
|
||||
|
||||
{props.children}
|
||||
</div>
|
||||
</TabsContext.Provider>;
|
||||
}
|
||||
|
||||
export const Tab: ParentComponent<{ label: string }> = (props) => {
|
||||
const id = `tab-${createUniqueId()}`;
|
||||
const context = useContext(TabsContext);
|
||||
export const Tab: ParentComponent<{ id: string, label: string }> = (props) => {
|
||||
const context = useTabs();
|
||||
|
||||
if (!context) {
|
||||
return undefined;
|
||||
}
|
||||
const isActive = context.register(props.id, props.label);
|
||||
const resolved = children(() => props.children);
|
||||
|
||||
return <details class={css.tab} id={id} open={context.active() === id} ontoggle={(e: ToggleEvent) => e.newState === 'open' && context.activate(id)}>
|
||||
<summary>{props.label}</summary>
|
||||
|
||||
{props.children}
|
||||
</details>
|
||||
return <Show when={isActive()}>{resolved()}</Show>;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, createUniqueId, For, JSX, ParentComponent, useContext } from "solid-js";
|
||||
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, createUniqueId, For, JSX, ParentComponent, splitProps, useContext } from "solid-js";
|
||||
import { CommandType } from "./index";
|
||||
import css from "./contextMenu.module.css";
|
||||
|
||||
|
@ -69,16 +69,19 @@ const Menu: Component<{ children: (command: CommandType) => JSX.Element }> = (pr
|
|||
</ul>;
|
||||
};
|
||||
|
||||
const Handle: ParentComponent = (props) => {
|
||||
const context = useContext(ContextMenu)!;
|
||||
const Handle: ParentComponent<Record<string, any>> = (props) => {
|
||||
const [local, rest] = splitProps(props, ['children']);
|
||||
|
||||
return <span style={`anchor-name: --context-menu-handle-${createUniqueId()};`} oncontextmenu={(e) => {
|
||||
const context = useContext(ContextMenu)!;
|
||||
const [handle, setHandle] = createSignal<HTMLElement>();
|
||||
|
||||
return <span {...rest} ref={setHandle} style={`anchor-name: --context-menu-handle-${createUniqueId()};`} oncontextmenu={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
context.show(e.target as HTMLElement);
|
||||
context.show(handle()!);
|
||||
|
||||
return false;
|
||||
}}>{props.children}</span>;
|
||||
}}>{local.children}</span>;
|
||||
};
|
||||
|
||||
export const Context = { Root, Menu, Handle };
|
|
@ -42,7 +42,6 @@ export const noop = Object.defineProperties(createCommand('noop', () => { }), {
|
|||
},
|
||||
}) as CommandType & { withLabel(label: string): CommandType };
|
||||
|
||||
|
||||
export const Command: Component<{ command: CommandType }> = (props) => {
|
||||
return <>
|
||||
{props.command.label}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { Accessor, Component, createContext, createEffect, createMemo, createSignal, For, onMount, ParentComponent, Show, useContext } from "solid-js";
|
||||
import { Accessor, Component, createContext, createEffect, createMemo, createRenderEffect, createSignal, For, onMount, ParentComponent, Show, useContext } from "solid-js";
|
||||
import { createStore, unwrap } from "solid-js/store";
|
||||
import { SelectionProvider, useSelection, selectable } from "../selectable";
|
||||
import { debounce, deepCopy, deepDiff, Mutation } from "~/utilities";
|
||||
|
@ -160,7 +160,7 @@ const Row: Component<{ entry: Entry, path?: string[] }> = (props) => {
|
|||
return <Show when={isLeaf(value)} fallback={<Group key={key} entry={value as Entry} path={path} />}>
|
||||
<div class={css.row} use:selectable={{ value, key: k }}>
|
||||
<div class={css.cell}>
|
||||
<input type="checkbox" checked={isSelected()} oninput={() => context.select([k])} />
|
||||
<input type="checkbox" checked={isSelected()} on:input={() => context.select([k])} />
|
||||
</div>
|
||||
|
||||
<div class={css.cell}>
|
||||
|
@ -187,16 +187,22 @@ const Group: Component<{ key: string, entry: Entry, path: string[] }> = (props)
|
|||
};
|
||||
|
||||
const TextArea: Component<{ key: string, value: string, lang: string, oninput?: (event: InputEvent) => any }> = (props) => {
|
||||
let element: HTMLTextAreaElement;
|
||||
const [element, setElement] = createSignal<HTMLTextAreaElement>();
|
||||
|
||||
const resize = () => {
|
||||
element.style.height = `1px`;
|
||||
element.style.height = `${2 + element.scrollHeight}px`;
|
||||
const el = element();
|
||||
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
|
||||
el.style.height = `1px`;
|
||||
el.style.height = `${2 + element()!.scrollHeight}px`;
|
||||
};
|
||||
|
||||
const mutate = debounce(() => {
|
||||
props.oninput?.(new InputEvent('input', {
|
||||
data: element.value.trim(),
|
||||
data: element()?.value.trim(),
|
||||
}))
|
||||
}, 300);
|
||||
|
||||
|
@ -205,12 +211,14 @@ const TextArea: Component<{ key: string, value: string, lang: string, oninput?:
|
|||
mutate();
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
createEffect(() => {
|
||||
props.value;
|
||||
|
||||
resize();
|
||||
});
|
||||
|
||||
return <textarea
|
||||
ref={element}
|
||||
ref={setElement}
|
||||
value={props.value}
|
||||
lang={props.lang}
|
||||
placeholder={props.lang}
|
||||
|
@ -218,6 +226,5 @@ const TextArea: Component<{ key: string, value: string, lang: string, oninput?:
|
|||
spellcheck
|
||||
wrap="soft"
|
||||
onkeyup={onKeyUp}
|
||||
on:pointerdown={(e: PointerEvent) => e.stopPropagation()}
|
||||
/>
|
||||
};
|
|
@ -1,13 +1,11 @@
|
|||
import Dexie, { EntityTable } from "dexie";
|
||||
import { createContext, useContext } from "solid-js";
|
||||
import { createContext, ParentComponent, useContext } from "solid-js";
|
||||
import { isServer } from "solid-js/web";
|
||||
import * as json from './parser/json';
|
||||
|
||||
type Handle = FileSystemFileHandle | FileSystemDirectoryHandle;
|
||||
|
||||
interface FileEntity {
|
||||
name: string;
|
||||
handle: Handle;
|
||||
handle: FileSystemDirectoryHandle;
|
||||
}
|
||||
|
||||
type Store = Dexie & {
|
||||
|
@ -15,9 +13,9 @@ type Store = Dexie & {
|
|||
};
|
||||
|
||||
interface FilesContextType {
|
||||
set(name: string, handle: Handle): Promise<void>;
|
||||
get(name: string): Promise<Handle | undefined>;
|
||||
list(): Promise<Handle[]>;
|
||||
set(name: string, handle: FileSystemDirectoryHandle): Promise<void>;
|
||||
get(name: string): Promise<FileSystemDirectoryHandle | undefined>;
|
||||
list(): Promise<FileSystemDirectoryHandle[]>;
|
||||
}
|
||||
|
||||
const FilesContext = createContext<FilesContextType>();
|
||||
|
@ -30,7 +28,7 @@ const clientContext = (): FilesContextType => {
|
|||
});
|
||||
|
||||
return {
|
||||
async set(name: string, handle: Handle) {
|
||||
async set(name: string, handle: FileSystemDirectoryHandle) {
|
||||
await db.files.put({ name, handle });
|
||||
},
|
||||
async get(name: string) {
|
||||
|
@ -52,11 +50,11 @@ const serverContext = (): FilesContextType => ({
|
|||
return Promise.resolve(undefined);
|
||||
},
|
||||
list() {
|
||||
return Promise.resolve<Handle[]>([]);
|
||||
return Promise.resolve<FileSystemDirectoryHandle[]>([]);
|
||||
},
|
||||
});
|
||||
|
||||
export const FilesProvider = (props) => {
|
||||
export const FilesProvider: ParentComponent = (props) => {
|
||||
const ctx = isServer ? serverContext() : clientContext();
|
||||
|
||||
return <FilesContext.Provider value={ctx}>{props.children}</FilesContext.Provider>;
|
||||
|
|
|
@ -2,6 +2,7 @@ import { Accessor, children, createContext, createEffect, createMemo, createRend
|
|||
import { createStore } from "solid-js/store";
|
||||
import { isServer } from "solid-js/web";
|
||||
import css from "./index.module.css";
|
||||
import { isFocusable } from "~/utilities";
|
||||
|
||||
enum Modifier {
|
||||
None = 0,
|
||||
|
@ -68,6 +69,8 @@ export const SelectionProvider: ParentComponent<{ selection?: SelectionHandler,
|
|||
mode = SelectionMode.Toggle;
|
||||
}
|
||||
|
||||
console.log(selection, mode);
|
||||
|
||||
setState('selection', existing => {
|
||||
switch (mode) {
|
||||
case SelectionMode.Toggle: {
|
||||
|
@ -230,7 +233,9 @@ export const selectable = (element: HTMLElement, options: Accessor<{ value: obje
|
|||
element.dataset.selected = isSelected() ? 'true' : undefined;
|
||||
});
|
||||
|
||||
const onPointerDown = (e: PointerEvent) => {
|
||||
const onPointerDown = (e: Event) => {
|
||||
// TODO :: find out if the cell clicked is editable and early exit after that
|
||||
|
||||
const [latest, setLatest] = internal.latest
|
||||
const [modifier] = internal.modifier
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { children, createEffect, createMemo, createResource, createSignal, createUniqueId, onMount, ParentProps } from "solid-js";
|
||||
import { MutarionKind, splitAt } from "~/utilities";
|
||||
import { children, Component, createEffect, createMemo, createResource, createSignal, createUniqueId, For, onMount, ParentProps, Show } from "solid-js";
|
||||
import { filter, MutarionKind, splitAt } from "~/utilities";
|
||||
import { Sidebar } from "~/components/sidebar";
|
||||
import { emptyFolder, FolderEntry, walk as fileTreeWalk, Tree, FileEntry, Entry } from "~/components/filetree";
|
||||
import { Menu } from "~/features/menu";
|
||||
|
@ -7,7 +7,7 @@ import { Grid, load, useFiles } from "~/features/file";
|
|||
import { Command, Context, createCommand, Modifier, noop } from "~/features/command";
|
||||
import { GridApi } from "~/features/file/grid";
|
||||
import css from "./edit.module.css";
|
||||
import { match } from "ts-pattern";
|
||||
import { Tab, Tabs } from "~/components/tabs";
|
||||
|
||||
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()) {
|
||||
|
@ -52,10 +52,9 @@ const findFile = (folder: FolderEntry, id: string) => {
|
|||
|
||||
export default function Edit(props: ParentProps) {
|
||||
const filesContext = useFiles();
|
||||
const [root, { mutate, refetch }] = createResource(() => filesContext?.get('root'));
|
||||
const [root, { refetch: getRoot, mutate: updateRoot }] = createResource(() => filesContext?.get('__root__'));
|
||||
const [tabs, { refetch: getTabs }] = createResource<FileSystemDirectoryHandle[]>(async () => (await filesContext?.list()) ?? [], { initialValue: [], ssrLoadFrom: 'initial' });
|
||||
const [tree, setFiles] = createSignal<FolderEntry>(emptyFolder);
|
||||
const [columns, setColumns] = createSignal<string[]>([]);
|
||||
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 [api, setApi] = createSignal<GridApi>();
|
||||
|
||||
|
@ -75,7 +74,8 @@ export default function Edit(props: ParentProps) {
|
|||
|
||||
// Since the files are stored in indexedDb we need to refetch on the client in order to populate on page load
|
||||
onMount(() => {
|
||||
refetch();
|
||||
getRoot();
|
||||
getTabs();
|
||||
});
|
||||
|
||||
createEffect(async () => {
|
||||
|
@ -100,10 +100,8 @@ export default function Edit(props: ParentProps) {
|
|||
return aggregate;
|
||||
}, new Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>());
|
||||
|
||||
setFiles({ name: '', id: '', kind: 'folder', entries: await Array.fromAsync(fileTreeWalk(directory)) });
|
||||
setColumns(['key', ...languages]);
|
||||
setFiles({ name: directory.name, id: '', kind: 'folder', entries: await Array.fromAsync(fileTreeWalk(directory)) });
|
||||
setEntries(merged);
|
||||
setRows(new Map(merged.entries().map(([key, langs]) => [key, Object.fromEntries(Object.entries(langs).map(([lang, { value }]) => [lang, value]))] as const)));
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -129,8 +127,8 @@ export default function Edit(props: ParentProps) {
|
|||
openFolder: createCommand('open folder', async () => {
|
||||
const directory = await window.showDirectoryPicker({ mode: 'readwrite' });
|
||||
|
||||
filesContext.set('root', directory);
|
||||
mutate(directory);
|
||||
filesContext.set('__root__', directory);
|
||||
updateRoot(directory);
|
||||
}),
|
||||
save: createCommand('save', async () => {
|
||||
const mutations = api()?.mutations() ?? [];
|
||||
|
@ -153,64 +151,19 @@ export default function Edit(props: ParentProps) {
|
|||
// 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 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];
|
||||
|
||||
// TODO :: try to resolve to a file
|
||||
//
|
||||
// | | entry | localEntry | id | file |
|
||||
// |---|-------!------------|----!------!
|
||||
// | 1 | x | x | x | x |
|
||||
// | 2 | x | x | x | |
|
||||
// | 3 | x | x | | |
|
||||
// | 4 | x | | | |
|
||||
// | 5 | | | | |
|
||||
//
|
||||
// 1. happy path
|
||||
// 2. weird edge case
|
||||
// 3. this language never had a file before. peek at at another language and create a new file with a mathing path
|
||||
// 4. error?
|
||||
// 5. error?
|
||||
|
||||
if (!localEntry) {
|
||||
throw new Error('invalid edge case???');
|
||||
}
|
||||
|
||||
const [handle, path = []] = await (async () => {
|
||||
if (localEntry.id === undefined) {
|
||||
const [, alternativeLocalEntry] = Object.entries(entry).find(([l, e]) => l !== lang && e.id !== undefined) ?? [];
|
||||
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 { directory, path } = alternativeLocalEntry ? findFile(tree(), alternativeLocalEntry.id) ?? {} : {};
|
||||
|
||||
// Short circuit if the mutation type is delete.
|
||||
// Otherwise we would create a new file handle,
|
||||
// and then immediately remove it again.
|
||||
if (mutation.kind === MutarionKind.Delete) {
|
||||
return [undefined, path] as const;
|
||||
}
|
||||
|
||||
const handle = await window.showSaveFilePicker({
|
||||
suggestedName: `${lang}.json`,
|
||||
startIn: directory ?? root(),
|
||||
excludeAcceptAllOption: true,
|
||||
types: [
|
||||
{ accept: { 'application/json': ['.json'] }, description: 'JSON' },
|
||||
]
|
||||
});
|
||||
|
||||
return [handle, path] as const;
|
||||
}
|
||||
|
||||
const { handle, path } = findFile(tree(), localEntry.id) ?? {};
|
||||
|
||||
return [handle, path] as const;
|
||||
})();
|
||||
|
||||
console.log(k, path.join('.'));
|
||||
|
||||
const createNewFile = async (lang: string, directory: FileSystemDirectoryHandle) => {
|
||||
const handle = await window.showSaveFilePicker({
|
||||
suggestedName: `${lang}.json`,
|
||||
startIn: directory,
|
||||
|
@ -219,36 +172,22 @@ export default function Edit(props: ParentProps) {
|
|||
{ accept: { 'application/json': ['.json'] }, description: 'JSON' },
|
||||
]
|
||||
});
|
||||
|
||||
// TODO :: patch the tree with this new entry
|
||||
// console.log(localEntry, tree());
|
||||
|
||||
return { handle, path };
|
||||
};
|
||||
|
||||
const key = k.slice(path.join('.').length);
|
||||
const { handle, path } = findFile(tree(), localEntry.id) ?? {};
|
||||
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];
|
||||
|
||||
const result = match([handle !== undefined, mutation.kind])
|
||||
.with([true, MutarionKind.Create], () => ({ action: MutarionKind.Create, key, value: rows[key][lang], handle }))
|
||||
.with([false, MutarionKind.Create], () => '2')
|
||||
.with([true, MutarionKind.Update], () => ({ action: MutarionKind.Update, key, value: rows[key][lang], handle }))
|
||||
.with([false, MutarionKind.Update], () => '4')
|
||||
.with([true, MutarionKind.Delete], () => ({ action: MutarionKind.Delete, key, handle }))
|
||||
.with([false, MutarionKind.Delete], () => '6')
|
||||
.exhaustive();
|
||||
return { action: mutation.kind, key, id, value, handle };
|
||||
}));
|
||||
|
||||
console.log(mutation, key, lang, entry, 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
|
||||
// }
|
||||
console.log(rows, entries(), Object.groupBy(fileMutations, m => m.id ?? 'undefined'))
|
||||
}, { key: 's', modifier: Modifier.Control }),
|
||||
saveAs: createCommand('save as', (handle?: FileSystemFileHandle) => {
|
||||
console.log('save as ...', handle);
|
||||
|
@ -298,17 +237,83 @@ export default function Edit(props: ParentProps) {
|
|||
<Menu.Item command={noop.withLabel('view')} />
|
||||
</Menu.Root>
|
||||
|
||||
<Sidebar as="aside" class={css.sidebar}>
|
||||
<Tree entries={tree().entries}>{
|
||||
file => {
|
||||
const mutated = createMemo(() => mutatedFiles().has(file().id));
|
||||
<Sidebar as="aside" label={tree().name} class={css.sidebar}>
|
||||
<Show when={!root.loading && root()} fallback={<button onpointerdown={() => commands.openFolder()}>open a folder</button>}>
|
||||
<Tree entries={tree().entries}>{[
|
||||
folder => {
|
||||
return <span onDblClick={() => {
|
||||
filesContext?.set(folder().name, folder().handle);
|
||||
getTabs();
|
||||
}}>{folder().name}</span>;
|
||||
},
|
||||
file => {
|
||||
const mutated = createMemo(() => mutatedFiles().has(file().id));
|
||||
|
||||
return <Context.Handle><span classList={{ [css.mutated]: mutated() }}>{file().name}</span></Context.Handle>;
|
||||
}
|
||||
}</Tree>
|
||||
return <Context.Handle classList={{ [css.mutated]: mutated() }}>{file().name}</Context.Handle>;
|
||||
},
|
||||
] as const}</Tree>
|
||||
</Show>
|
||||
</Sidebar>
|
||||
|
||||
<Grid columns={columns()} rows={rows()} api={setApi} />
|
||||
<Tabs>
|
||||
<For each={tabs()}>{
|
||||
directory => <Tab id={createUniqueId()} label={directory.name}>{'some text in front'}<Kaas directory={directory} /></Tab>
|
||||
}</For>
|
||||
</Tabs>
|
||||
</Context.Root>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
const Kaas: Component<{ directory: FileSystemDirectoryHandle }> = (props) => {
|
||||
const filesContext = useFiles();
|
||||
const [root, { mutate, refetch }] = createResource(() => filesContext?.get('root'));
|
||||
const [columns, setColumns] = createSignal<string[]>([]);
|
||||
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 [api, setApi] = createSignal<GridApi>();
|
||||
|
||||
createEffect(() => {
|
||||
const directory = props.directory;
|
||||
|
||||
if (!directory) {
|
||||
return;
|
||||
}
|
||||
|
||||
(async () => {
|
||||
const contents = await Array.fromAsync(
|
||||
filter(directory.values(), (handle): handle is FileSystemFileHandle => handle.kind === 'file' && handle.name.endsWith('.json')),
|
||||
async handle => {
|
||||
const id = await handle.getUniqueId();
|
||||
const file = await handle.getFile();
|
||||
const lang = file.name.split('.').at(0)!;
|
||||
const entries = (await load(file))!;
|
||||
|
||||
return { id, handle, lang, entries };
|
||||
}
|
||||
);
|
||||
const languages = new Set(contents.map(c => c.lang));
|
||||
const template = contents.map(({ lang, handle }) => [lang, { handle, value: '' }]);
|
||||
|
||||
const merged = contents.reduce((aggregate, { id, handle, lang, entries }) => {
|
||||
for (const [key, value] of entries.entries()) {
|
||||
if (!aggregate.has(key)) {
|
||||
aggregate.set(key, Object.fromEntries(template));
|
||||
}
|
||||
|
||||
aggregate.get(key)![lang] = { value, handle, id };
|
||||
}
|
||||
|
||||
return aggregate;
|
||||
}, new Map<string, Record<string, { id: string, value: string, handle: FileSystemFileHandle }>>());
|
||||
|
||||
setColumns(['key', ...languages]);
|
||||
setRows(new Map(merged.entries().map(([key, langs]) => [key, Object.fromEntries(Object.entries(langs).map(([lang, { value }]) => [lang, value]))] as const)));
|
||||
})();
|
||||
});
|
||||
|
||||
createEffect(() => {
|
||||
// console.log(columns(), rows());
|
||||
});
|
||||
|
||||
return <Grid columns={columns()} rows={rows()} api={setApi} />;
|
||||
};
|
|
@ -127,5 +127,18 @@ const zip = function* (a: Iterable<readonly [string | number, any]>, b: Iterable
|
|||
|
||||
yield [entryA, entryB] as const;
|
||||
}
|
||||
};
|
||||
|
||||
export interface filter {
|
||||
<T, S extends T>(subject: AsyncIterableIterator<T>, predicate: (value: T) => value is S): AsyncGenerator<S, void, unknown>;
|
||||
<T>(subject: AsyncIterableIterator<T>, predicate: (value: T) => unknown): AsyncGenerator<T, void, unknown>;
|
||||
}
|
||||
|
||||
export const filter = async function*<T, S extends T>(subject: AsyncIterableIterator<T>, predicate: (value: T) => value is S): AsyncGenerator<S, void, unknown> {
|
||||
for await (const value of subject) {
|
||||
if (predicate(value)) {
|
||||
yield value;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue