Fix type changes, tag navigation, and routing cleanup

- set_entry removes entry from old bucket when type changes (fixes duplicates)
- PUT /api/entries/:id now accepts type field
- Tag clicks navigate via URL (#type/x/tag/y or #tag/y) instead of mutating state
- Replace magic 'all' hash with empty hash as the all-entries route
- Clickable tags in entry detail view
- Tag filter dropdown updates URL

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-24 15:56:42 +00:00
parent 25c8f98b7f
commit 40722310ae
3 changed files with 44 additions and 9 deletions

View File

@@ -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);

View File

@@ -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

View File

@@ -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; }