Add modal for changing title/tags
This commit is contained in:
@@ -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
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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 * 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
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user