Add modal for changing title/tags

This commit is contained in:
2025-09-13 18:15:02 -07:00
parent d730428eb8
commit 4279916f5b
6 changed files with 219 additions and 16 deletions
+12
View File
@@ -1,8 +1,20 @@
.file-link {
text-decoration: none;
}
.file { .file {
border: none; border: none;
background: none; background: none;
text-align: left; text-align: left;
width: 100%; width: 100%;
display: flex;
flex-direction: row;
padding: 0.25rem 1rem;
gap: 1rem;
}
.file .tags {
color: #999;
} }
.file:hover { .file:hover {
+72 -11
View File
@@ -1,29 +1,90 @@
import { For, onMount } from 'solid-js'; import { createSignal, For, onMount } from 'solid-js';
import './Chooser.css'; import './Chooser.css';
import { useNavbar } from './App'; import { useNavbar } from './App';
import { useNavigate } from '@solidjs/router'; import { A } from '@solidjs/router';
import { defaultVault } from '../sync'; import { defaultVault } from '../sync';
import { getDocsMap } from '../vault'; import { getDocsMap } from '../vault';
import DocInfoDialog, { type DocInfo } from './DocInfoDialog';
function Chooser() { function Chooser() {
const navigate = useNavigate();
const { setFileId } = useNavbar(); const { setFileId } = useNavbar();
onMount(() => setFileId(null)); onMount(() => setFileId(null));
const docsMap = getDocsMap(defaultVault.doc); const docsMap = getDocsMap(defaultVault.doc);
const titles: Record<string, string> = {};
for (const [id, docInfo] of docsMap.entries()) { const computeDocsList = () => {
titles[id] = docInfo.get('title'); const result: DocInfo[] = [];
for (const [id, entry] of docsMap.entries()) {
const title = entry.get('title');
const tags = Array.from(entry.get('tags').keys());
tags.sort();
result.push({ id, title, tags });
} }
return result;
};
const [getDocsList, setDocsList] = createSignal<DocInfo[]>(computeDocsList());
const [getDialogOpen, setDialogOpen] = createSignal(false);
const [getDocInfo, setDocInfo] = createSignal<DocInfo | null>(null);
const handleInfoUpdate = (info: DocInfo) => {
if (info.id === null) {
throw new Error('Got null doc ID while updating doc info');
}
const doc = docsMap.get(info.id)!;
// Update title
if (doc.get('title') !== info.title) {
doc.set('title', info.title);
}
// Update tags
const tagsMap = doc.get('tags');
const newTags = new Set(info.tags);
for (const key of tagsMap.keys()) {
if (!newTags.has(key)) {
tagsMap.delete(key);
}
}
for (const key of newTags) {
if (!tagsMap.has(key)) {
tagsMap.set(key, true);
}
}
setDocsList(computeDocsList());
};
return ( return (
<> <>
<For each={Object.entries(titles)}> <DocInfoDialog
{([id, title]: [string, string]) => ( initialDoc={getDocInfo()}
<button class="file" onclick={() => navigate(`doc/${id}`)}> onClose={(x) => {
{title} setDialogOpen(false);
{/* {availableFile === props.file() ? '(*)' : ''} */} if (x.action === 'save') {
handleInfoUpdate(x.data);
}
}}
open={getDialogOpen()}
/>
<For each={getDocsList()}>
{({ id, title, tags }) => (
<A href={`doc/${id}`} class="file-link">
<div class="file">
<div class="title">{title}</div>
<div class="tags">{tags.join(' ')}</div>
<button
onclick={(e) => {
e.stopPropagation();
e.preventDefault();
setDialogOpen(true);
setDocInfo({ id, title, tags });
}}
>
Edit info
</button> </button>
</div>
</A>
)} )}
</For> </For>
</> </>
+22
View File
@@ -0,0 +1,22 @@
.doc-info-modal .title {
width: 100%;
text-align: center;
font-weight: bold;
}
.doc-info-modal div {
display: flex;
flex-direction: row;
gap: 1rem;
margin-top: 0.5rem;
align-items: center;
}
.doc-info-modal input[type='text'] {
min-width: 20rem;
}
.doc-info-modal .buttons {
justify-content: center;
gap: 1rem;
}
+98
View File
@@ -0,0 +1,98 @@
import { createEffect, createSignal } from 'solid-js';
import './DocInfoDialog.css';
type Props = {
initialDoc: DocInfo | null;
onClose?: (x: Result<DocInfo>) => any;
open: boolean;
};
export type DocInfo = {
id: string | null;
title: string;
tags: string[];
};
type Result<T> = { action: 'save'; data: T } | { action: 'cancel' };
export default function DocInfoDialog(props: Props) {
let el!: HTMLDialogElement;
const [getTitle, setTitle] = createSignal('');
const [getTagsString, setTagsString] = createSignal('');
createEffect(() => setTitle(props.initialDoc?.title ?? ''));
createEffect(() => setTagsString(props.initialDoc?.tags.join(' ') ?? ''));
// Used to prevent double calls to props.onClose().
// This happens when the user clicks Save/Cancel (call #1) and the closing
// of the dialog triggers the dialog's own onClose (call #2), a handler which
// has to be in place to handle when the user hits Esc.
let submitted = true;
createEffect(() => {
if (!el) {
return;
}
if (props.open !== el.open) {
if (props.open) {
submitted = false;
el.showModal();
} else {
submitted = true;
el.close();
}
}
});
const handleSave = () => {
if (!submitted) {
props.onClose?.({
action: 'save',
data: {
id: props.initialDoc?.id ?? null,
title: getTitle(),
tags: getTagsString()
.split(' ')
.filter((x) => x !== ''),
},
});
}
submitted = true;
};
const handleCancel = () => {
if (!submitted) {
props.onClose?.({ action: 'cancel' });
}
submitted = true;
};
return (
<dialog ref={el} onClose={handleCancel} class="doc-info-modal">
<p class="title">
{props.initialDoc ? 'Edit document info' : 'Create new document'}
</p>
<div>
<label>Title</label>
<input
type="text"
value={getTitle()}
onChange={(e) => setTitle(e.target.value)}
/>
</div>
<div>
<label>Tags</label>
<input
type="text"
value={getTagsString()}
onChange={(e) => setTagsString(e.target.value)}
/>
</div>
<div class="buttons">
<button onClick={handleCancel}>Cancel</button>
<button onClick={handleSave}>Save</button>
</div>
</dialog>
);
}
+11 -2
View File
@@ -2,7 +2,7 @@ import _ from 'lodash';
import * as Y from 'yjs'; import * as Y from 'yjs';
import { openDB } from 'idb'; import { openDB } from 'idb';
import { generateId } from './util'; import { generateId } from './util';
import { getDocsMap, type DocMap } from './vault'; import { getDocsMap, getVaultMap, type DocMap } from './vault';
const DB_NAME = 'synced-docs'; const DB_NAME = 'synced-docs';
const LOCAL_UPDATE_STORE = 'local-updates'; const LOCAL_UPDATE_STORE = 'local-updates';
@@ -91,13 +91,22 @@ async function getOrCreateVaultDoc() {
throw new Error("Vault record didn't have id"); throw new Error("Vault record didn't have id");
} }
const vault = await LocalDocument.load(id); const vault = await LocalDocument.load(id);
const vaultMap = vault.doc.getMap(); const vaultMap = getVaultMap(vault.doc);
if (!vaultMap.has('id')) { if (!vaultMap.has('id')) {
vaultMap.set('id', id); vaultMap.set('id', id);
} }
if (!vaultMap.has('docs')) { if (!vaultMap.has('docs')) {
vaultMap.set('docs', new Y.Map()); vaultMap.set('docs', new Y.Map());
} }
// Make sure each doc has tags
const docsMap = vaultMap.get('docs');
for (const docMap of docsMap.values()) {
if (!docMap.has('tags')) {
docMap.set('tags', new Y.Map());
}
}
return vault; return vault;
} }
+2 -1
View File
@@ -14,8 +14,9 @@ type DocMapSchema = {
tags: Y.Map<boolean>; tags: Y.Map<boolean>;
}; };
type TypedMap<T> = Omit<Y.Map<any>, 'get'> & { type TypedMap<T> = Omit<Y.Map<any>, 'get' | 'has'> & {
get<K extends keyof T>(key: K): T[K]; get<K extends keyof T>(key: K): T[K];
has<K extends keyof T>(key: K): boolean;
}; };
export function getVaultMap(ydoc: Y.Doc): VaultMap { export function getVaultMap(ydoc: Y.Doc): VaultMap {