Move pages to and from the vault
A page, including a journal day, can be moved into the unlocked vault with its id kept. Links to it elsewhere become [[locked:id]], shown as Locked Page until the vault is unlocked, so its title is kept only in the vault. Move out reverses it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c08a8b8718
commit
994927c704
10 files changed
+443
-14
No files matched your search
@@ -47,14 +47,14 @@ async function boot() {
|
||||
if (e.status === 401) return showLogin(why);
|
||||
return $app.replaceChildren(h('div', { class: 'center' }, card('', brand(), h('p', {}, e.message), btn('Try again', 'restore', boot, 'btn'))));
|
||||
}
|
||||
await refreshPages().catch(() => { });
|
||||
await Promise.all([refreshPages().catch(() => { }), refreshVaultNames()]);
|
||||
S.ready = true;
|
||||
buildShell();
|
||||
show();
|
||||
}
|
||||
|
||||
function showLogin(why) {
|
||||
S.editors.clear(); S.view = null; document.title = 'Sign in · Mako';
|
||||
S.editors.clear(); S.view = null; S.vaultNames = null; document.title = 'Sign in · Mako';
|
||||
const next = location.hash.length > 2 ? location.hash : '';
|
||||
$app.replaceChildren(h('main', { class: 'center' }, card('', brand(),
|
||||
h('h1', { class: 'login-h' }, 'Your private notebook'),
|
||||
@@ -233,10 +233,25 @@ function pageView({ id, date, parent = '' }) {
|
||||
})),
|
||||
date ? null : btn('Move', 'move', () => have ? movePage(note, async () => { await refreshPages(); note = await api('GET', '/api/notes/' + note.id); drawCrumbs(); }) : toast('Type something first, then you can move the page')),
|
||||
date ? null : btn('Subpage', 'add', act(async () => { if (!have) return toast('Type something first, then you can add a subpage'); await ed.flush(); location.hash = '#/new/' + note.id; })),
|
||||
btn('To vault', 'lock', act(toVault), 'ghost'),
|
||||
btn('Delete', 'trash', act(trashIt), 'ghost danger'));
|
||||
}
|
||||
ed = null;
|
||||
|
||||
// Moving a page to the vault needs the vault to be unlocked. Links to the page elsewhere read "Locked Page".
|
||||
async function toVault() {
|
||||
if (!note || !note.id) return toast('Type something first, then you can move the page to the vault');
|
||||
await ed.flush();
|
||||
const v = await api('GET', '/api/vault');
|
||||
if (!v.unlocked) return toast(v.exists ? 'Unlock your vault first, then move the page.' : 'Make your vault first, then move the page.', false, { label: 'Open vault', fn: () => { location.hash = '#/vault'; } });
|
||||
const n = note;
|
||||
if (!await confirmDialog({ title: 'Move this page to the vault?', body: `"${n.daily ? dayLabel(n.daily) : n.name}" will be locked away in your vault. Pages that link to it will show "Locked Page" until you unlock the vault.`, confirm: 'Move to vault', danger: false })) return;
|
||||
await api('POST', `/api/notes/${n.id}/vault`);
|
||||
await Promise.all([refreshPages(), refreshVaultNames()]);
|
||||
toast('Moved to the vault', false, { label: 'Undo', fn: act(async () => { await api('POST', `${VAULT}/notes/${n.id}/out`); await Promise.all([refreshPages(), refreshVaultNames()]); location.hash = hrefOf(n); }) });
|
||||
location.hash = date ? '#/journal' : n.parent && S.byId.has(n.parent) ? '#/p/' + n.parent : '#/';
|
||||
}
|
||||
|
||||
async function trashIt() {
|
||||
if (!note) { location.hash = '#/'; return; }
|
||||
await ed.flush();
|
||||
|
||||
@@ -7,13 +7,24 @@ const INLINE_SRC = '`([^`\\n]+)`|\\*\\*([^*\\n]+)\\*\\*|(?<![\\w*])\\*([^*\\s][^
|
||||
const tagChip = t => h('a', { class: 'chip', href: '#/tag/' + encodeURIComponent(t) }, '#' + t);
|
||||
const extLink = (url, label) => h('a', { href: url, target: '_blank', rel: 'noopener noreferrer' }, label);
|
||||
|
||||
// A link to a page that was moved to the vault reads [[locked:id]]. It says "Locked Page" until the vault is
|
||||
// unlocked, and then shows the page's own name and opens it.
|
||||
function lockedLink(id) {
|
||||
const name = S.vaultNames?.get(id) ?? (S.vaultNames && id.startsWith('daily-') ? dayLabel(id.slice(6)) : null);
|
||||
if (name != null) return h('a', { class: 'wikilink in-vault', href: '#/vault/' + id }, icon('lock', 16), name);
|
||||
return h('a', { class: 'wikilink locked', href: '#/vault', title: 'This page is in your vault. Unlock the vault to open it.' }, icon('lock', 16), 'Locked Page');
|
||||
}
|
||||
|
||||
function wikiLink(target, text) {
|
||||
const title = target.trim(), page = pageByTitle(title);
|
||||
const title = target.trim();
|
||||
if (/^locked:[a-z0-9-]+$/.test(title)) return lockedLink(title.slice(7));
|
||||
const page = pageByTitle(title);
|
||||
if (page) return h('a', { class: 'wikilink', href: '#/p/' + page.id }, text || title);
|
||||
return h('a', { class: 'wikilink missing', href: '#/link/' + encodeURIComponent(title), title: 'No page called "' + title + '" yet. Click to create it.' }, text || title);
|
||||
}
|
||||
|
||||
// With plain set (the vault) [[links]] and #tags stay as typed, so a page there never points into the other notebook.
|
||||
// Only a link to another page in the vault, which is [[locked:id]], is shown as a link.
|
||||
function inline(text, plain) {
|
||||
const re = new RegExp(INLINE_SRC, 'gu'), out = [];
|
||||
let last = 0, m;
|
||||
@@ -23,7 +34,7 @@ function inline(text, plain) {
|
||||
if (m[1] != null) out.push(h('code', {}, m[1]));
|
||||
else if (m[2] != null) out.push(h('strong', {}, inline(m[2], plain)));
|
||||
else if (m[3] != null) out.push(h('em', {}, inline(m[3], plain)));
|
||||
else if (m[4] != null) out.push(plain ? m[0] : wikiLink(m[4], m[5]));
|
||||
else if (m[4] != null) out.push(plain && !m[4].trim().startsWith('locked:') ? m[0] : wikiLink(m[4], m[5]));
|
||||
else if (m[6] != null) out.push(extLink(m[7], m[6]));
|
||||
else if (m[8] != null) out.push(extLink(m[8], m[8]));
|
||||
else {
|
||||
|
||||
@@ -347,3 +347,5 @@
|
||||
.vault-row:hover { background:var(--accent-soft); }
|
||||
.vault-name { display:flex; align-items:center; gap:var(--s2); flex:1; min-width:0; color:var(--text); text-decoration:none; }
|
||||
.clip1 { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.wikilink .icon { vertical-align:-2px; margin-right:var(--s1); }
|
||||
.wikilink.locked { color:var(--text-muted); border-bottom-style:dotted; border-bottom-color:var(--text-faint); }
|
||||
@@ -6,6 +6,17 @@
|
||||
// or download, and they cannot link to it.
|
||||
const VAULT = '/api/vault';
|
||||
|
||||
// S.vaultNames maps the id of each vault page to its name while the vault is unlocked, so that a
|
||||
// [[locked:id]] link elsewhere can say what it points at. It is null while the vault is locked.
|
||||
async function refreshVaultNames() {
|
||||
try {
|
||||
const v = await api('GET', VAULT);
|
||||
if (!v.unlocked) { S.vaultNames = null; return; }
|
||||
const r = await api('GET', VAULT + '/pages');
|
||||
S.vaultNames = new Map(r.pages.map(p => [p.id, p.name]));
|
||||
} catch { S.vaultNames = null; }
|
||||
}
|
||||
|
||||
function vaultView(arg) {
|
||||
const root = h('div', { class: 'page vault' });
|
||||
let ed = null, dead = false;
|
||||
@@ -14,6 +25,7 @@ function vaultView(arg) {
|
||||
const lock = async () => {
|
||||
await flushAll();
|
||||
await api('POST', VAULT + '/lock');
|
||||
S.vaultNames = null;
|
||||
location.hash = '#/vault';
|
||||
show();
|
||||
};
|
||||
@@ -115,9 +127,9 @@ function vaultView(arg) {
|
||||
h('a', { href: '#/vault' }, 'Vault'));
|
||||
const meta = h('p', { class: 'sub page-meta' });
|
||||
ed = pageEditor({
|
||||
note, vault: true,
|
||||
onCreated(c) { note = c; history.replaceState(null, '', '#/vault/' + c.id); document.title = (c.name || 'New page') + ' · Vault · Mako'; draw(); },
|
||||
onSaved(c) { note = c; meta.textContent = 'Edited ' + fmtTime(c.updated); document.title = (c.name || 'New page') + ' · Vault · Mako'; },
|
||||
note, vault: true, fixedTitle: note?.daily ? note.title : undefined, // a journal day is named by its date
|
||||
onCreated(c) { note = c; history.replaceState(null, '', '#/vault/' + c.id); document.title = (c.name || 'New page') + ' · Vault · Mako'; draw(); refreshVaultNames(); },
|
||||
onSaved(c, { renamed }) { note = c; meta.textContent = 'Edited ' + fmtTime(c.updated); document.title = (c.name || 'New page') + ' · Vault · Mako'; if (renamed) refreshVaultNames(); },
|
||||
});
|
||||
ed.onMode = () => draw();
|
||||
view.toggleMode = () => ed.setMode(ed.mode === 'read' ? ed.lastEdit : 'read');
|
||||
@@ -126,6 +138,15 @@ function vaultView(arg) {
|
||||
[['live', 'Live', 'Rendered, except the line you are on'], ['write', 'Edit', 'Plain markdown'], ['read', 'View', 'The finished page']].map(([m, label, tip]) =>
|
||||
h('button', { class: 'seg-b' + (ed.mode === m ? ' on' : ''), 'aria-pressed': String(ed.mode === m), title: tip, onclick: () => ed.setMode(m) }, label)));
|
||||
fill(actions, ed.status, h('span', { class: 'grow' }), seg, btn('Lock', 'lock', act(lock)),
|
||||
btn('Move out', 'move', act(async () => {
|
||||
if (!note || !note.id) return toast('Type something first, then you can move the page out');
|
||||
await ed.flush();
|
||||
const n = note;
|
||||
await api('POST', `${VAULT}/notes/${n.id}/out`);
|
||||
await Promise.all([refreshPages(), refreshVaultNames()]);
|
||||
toast('Moved out of the vault', false, { label: 'Undo', fn: act(async () => { await api('POST', `/api/notes/${n.id}/vault`); await Promise.all([refreshPages(), refreshVaultNames()]); location.hash = '#/vault/' + n.id; }) });
|
||||
location.hash = hrefOf(n);
|
||||
})),
|
||||
btn('Delete', 'trash', act(async () => {
|
||||
if (!note) { location.hash = '#/vault'; return; }
|
||||
await ed.flush();
|
||||
@@ -152,7 +173,8 @@ function vaultView(arg) {
|
||||
const v = await api('GET', VAULT);
|
||||
if (dead) return;
|
||||
if (!v.exists) return create();
|
||||
if (!v.unlocked) return unlock();
|
||||
if (!v.unlocked) { S.vaultNames = null; return unlock(); }
|
||||
refreshVaultNames();
|
||||
return arg ? page(arg) : list(false);
|
||||
});
|
||||
return view;
|
||||
|
||||
Reference in new issue
Block a user