From 4279916f5b32eb849f20aa1ae56508c73213bd4f Mon Sep 17 00:00:00 2001 From: Chris Mounce Date: Sat, 13 Sep 2025 18:15:02 -0700 Subject: [PATCH] Add modal for changing title/tags --- src/components/Chooser.css | 12 ++++ src/components/Chooser.tsx | 87 +++++++++++++++++++++++----- src/components/DocInfoDialog.css | 22 +++++++ src/components/DocInfoDialog.tsx | 98 ++++++++++++++++++++++++++++++++ src/sync.ts | 13 ++++- src/vault.ts | 3 +- 6 files changed, 219 insertions(+), 16 deletions(-) create mode 100644 src/components/DocInfoDialog.css create mode 100644 src/components/DocInfoDialog.tsx diff --git a/src/components/Chooser.css b/src/components/Chooser.css index d619346..3e539f5 100644 --- a/src/components/Chooser.css +++ b/src/components/Chooser.css @@ -1,8 +1,20 @@ +.file-link { + text-decoration: none; +} + .file { border: none; background: none; text-align: left; width: 100%; + display: flex; + flex-direction: row; + padding: 0.25rem 1rem; + gap: 1rem; +} + +.file .tags { + color: #999; } .file:hover { diff --git a/src/components/Chooser.tsx b/src/components/Chooser.tsx index e5f1d66..02b55b0 100644 --- a/src/components/Chooser.tsx +++ b/src/components/Chooser.tsx @@ -1,29 +1,90 @@ -import { For, onMount } from 'solid-js'; +import { createSignal, For, onMount } from 'solid-js'; import './Chooser.css'; import { useNavbar } from './App'; -import { useNavigate } from '@solidjs/router'; +import { A } from '@solidjs/router'; import { defaultVault } from '../sync'; import { getDocsMap } from '../vault'; +import DocInfoDialog, { type DocInfo } from './DocInfoDialog'; function Chooser() { - const navigate = useNavigate(); const { setFileId } = useNavbar(); onMount(() => setFileId(null)); const docsMap = getDocsMap(defaultVault.doc); - const titles: Record = {}; - for (const [id, docInfo] of docsMap.entries()) { - titles[id] = docInfo.get('title'); - } + + const computeDocsList = () => { + 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(computeDocsList()); + + const [getDialogOpen, setDialogOpen] = createSignal(false); + const [getDocInfo, setDocInfo] = createSignal(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 ( <> - - {([id, title]: [string, string]) => ( - + { + setDialogOpen(false); + if (x.action === 'save') { + handleInfoUpdate(x.data); + } + }} + open={getDialogOpen()} + /> + + {({ id, title, tags }) => ( + +
+
{title}
+
{tags.join(' ')}
+ +
+
)}
diff --git a/src/components/DocInfoDialog.css b/src/components/DocInfoDialog.css new file mode 100644 index 0000000..f6ae15c --- /dev/null +++ b/src/components/DocInfoDialog.css @@ -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; +} diff --git a/src/components/DocInfoDialog.tsx b/src/components/DocInfoDialog.tsx new file mode 100644 index 0000000..de5dcdb --- /dev/null +++ b/src/components/DocInfoDialog.tsx @@ -0,0 +1,98 @@ +import { createEffect, createSignal } from 'solid-js'; +import './DocInfoDialog.css'; + +type Props = { + initialDoc: DocInfo | null; + onClose?: (x: Result) => any; + open: boolean; +}; + +export type DocInfo = { + id: string | null; + title: string; + tags: string[]; +}; + +type Result = { 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 ( + +

+ {props.initialDoc ? 'Edit document info' : 'Create new document'} +

+
+ + setTitle(e.target.value)} + /> +
+ +
+ + setTagsString(e.target.value)} + /> +
+ +
+ + +
+
+ ); +} diff --git a/src/sync.ts b/src/sync.ts index 536e396..b9410c2 100644 --- a/src/sync.ts +++ b/src/sync.ts @@ -2,7 +2,7 @@ import _ from 'lodash'; import * as Y from 'yjs'; import { openDB } from 'idb'; import { generateId } from './util'; -import { getDocsMap, type DocMap } from './vault'; +import { getDocsMap, getVaultMap, type DocMap } from './vault'; const DB_NAME = 'synced-docs'; const LOCAL_UPDATE_STORE = 'local-updates'; @@ -91,13 +91,22 @@ async function getOrCreateVaultDoc() { throw new Error("Vault record didn't have id"); } const vault = await LocalDocument.load(id); - const vaultMap = vault.doc.getMap(); + const vaultMap = getVaultMap(vault.doc); if (!vaultMap.has('id')) { vaultMap.set('id', id); } if (!vaultMap.has('docs')) { 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; } diff --git a/src/vault.ts b/src/vault.ts index 330aa7b..bcc261a 100644 --- a/src/vault.ts +++ b/src/vault.ts @@ -14,8 +14,9 @@ type DocMapSchema = { tags: Y.Map; }; -type TypedMap = Omit, 'get'> & { +type TypedMap = Omit, 'get' | 'has'> & { get(key: K): T[K]; + has(key: K): boolean; }; export function getVaultMap(ydoc: Y.Doc): VaultMap {