diff --git a/lib/storage.mjs b/lib/storage.mjs index 2a69bcb..262f7bd 100644 --- a/lib/storage.mjs +++ b/lib/storage.mjs @@ -127,6 +127,15 @@ export function get_entry(id) { } export function set_entry(entry) { + // Remove from any other bucket in case type changed + for (const t of Object.keys(store.get('entry_types') ?? {})) { + if (t === entry.type) { continue; } + const bucket = store.get(t); + if (bucket?.[entry.id]) { + delete bucket[entry.id]; + store.set(t, bucket); + } + } const bucket = store.get(entry.type) ?? {}; bucket[entry.id] = entry; store.set(entry.type, bucket); diff --git a/public/app.mjs b/public/app.mjs index f752c44..e70f1d4 100644 --- a/public/app.mjs +++ b/public/app.mjs @@ -289,8 +289,8 @@ function make_entry_row(entry, children_map) { span.title = `Filter by tag: ${tag}`; span.addEventListener('click', (e) => { e.stopPropagation(); - state.filter_tag = tag; - render(); + const base = state.active_type ? 'type/' + state.active_type : ''; + navigate(base ? base + '/tag/' + tag : 'tag/' + tag); }); tags_el.appendChild(span); } @@ -337,7 +337,7 @@ function render_nav() { const all_btn = document.createElement('button'); all_btn.className = 'nav-btn' + (state.active_view === 'entries' && state.active_type === null ? ' active' : ''); all_btn.textContent = 'All'; - all_btn.addEventListener('click', () => navigate('all')); + all_btn.addEventListener('click', () => navigate('')); nav.appendChild(all_btn); for (const et of state.entry_types) { @@ -432,7 +432,11 @@ function render_entries(container) { if (tag === state.filter_tag) { opt.selected = true; } tag_sel.appendChild(opt); } - tag_sel.addEventListener('change', () => { state.filter_tag = tag_sel.value; render(); }); + tag_sel.addEventListener('change', () => { + const base = state.active_type ? 'type/' + state.active_type : ''; + const tag_hash = tag_sel.value ? (base ? base + '/tag/' + tag_sel.value : 'tag/' + tag_sel.value) : base; + navigate(tag_hash); + }); filter_bar.appendChild(tag_sel); const search_input = document.createElement('input'); @@ -590,11 +594,27 @@ function apply_hash(hash) { state.active_type = null; state.active_entry_id = null; state.active_edit = null; - } else if (hash.startsWith('type/')) { + } else if (hash === '' || hash.startsWith('type/') || hash.startsWith('tag/')) { state.active_view = 'entries'; - state.active_type = hash.slice(5); state.active_entry_id = null; state.active_edit = null; + if (hash.startsWith('type/')) { + const rest = hash.slice(5); + const tag_idx = rest.indexOf('/tag/'); + if (tag_idx !== -1) { + state.active_type = rest.slice(0, tag_idx); + state.filter_tag = rest.slice(tag_idx + 5); + } else { + state.active_type = rest; + state.filter_tag = ''; + } + } else if (hash.startsWith('tag/')) { + state.active_type = null; + state.filter_tag = hash.slice(4); + } else { + state.active_type = null; + state.filter_tag = ''; + } } else if (/^\d+$/.test(hash)) { state.active_view = 'entry'; state.active_entry_id = Number(hash); @@ -733,8 +753,13 @@ function render_entry_detail(container) { for (const tag of entry.tags) { const span = document.createElement('span'); - span.className = 'tag'; + span.className = 'tag clickable-tag'; span.textContent = tag; + span.title = `Filter by tag: ${tag}`; + span.addEventListener('click', () => { + const base = entry.type ? 'type/' + entry.type : ''; + navigate(base ? base + '/tag/' + tag : 'tag/' + tag); + }); meta.appendChild(span); } @@ -790,7 +815,7 @@ function render_edit_view(container) { return; } - const back_hash = ae.mode === 'edit' ? String(ae.id) : (ae.type ? 'type/' + ae.type : 'all'); + const back_hash = ae.mode === 'edit' ? String(ae.id) : (ae.type ? 'type/' + ae.type : ''); let current_parent_id = ae.parent_id ?? entry?.parent_id ?? null; // View wrapper diff --git a/server.mjs b/server.mjs index 98af613..d9b75a1 100644 --- a/server.mjs +++ b/server.mjs @@ -122,8 +122,9 @@ app.get('/api/entries/:id', (req, res) => { app.put('/api/entries/:id', (req, res) => { const existing = get_entry(Number(req.params.id)); if (!existing) { return fail(res, 'not found', 404); } - const { title, body, status, priority, tags, parent_id } = req.body; + const { type, title, body, status, priority, tags, parent_id } = req.body; const updated = { ...existing, updated_at: Date.now() }; + if (type !== undefined && get_entry_type(type)) { updated.type = type; } if (title !== undefined) { updated.title = title.trim(); } if (body !== undefined) { updated.body = body.trim(); } if (status !== undefined) { updated.status = status; }