Add modal for changing title/tags
This commit is contained in:
@@ -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 {
|
||||
|
||||
+74
-13
@@ -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<string, string> = {};
|
||||
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<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 (
|
||||
<>
|
||||
<For each={Object.entries(titles)}>
|
||||
{([id, title]: [string, string]) => (
|
||||
<button class="file" onclick={() => navigate(`doc/${id}`)}>
|
||||
{title}
|
||||
{/* {availableFile === props.file() ? '(*)' : ''} */}
|
||||
</button>
|
||||
<DocInfoDialog
|
||||
initialDoc={getDocInfo()}
|
||||
onClose={(x) => {
|
||||
setDialogOpen(false);
|
||||
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>
|
||||
</div>
|
||||
</A>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -14,8 +14,9 @@ type DocMapSchema = {
|
||||
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];
|
||||
has<K extends keyof T>(key: K): boolean;
|
||||
};
|
||||
|
||||
export function getVaultMap(ydoc: Y.Doc): VaultMap {
|
||||
|
||||
Reference in New Issue
Block a user