Add a tags screen
Screen 8 lists every distinct tag -- the ones the index holds and the ones rules.toml names -- with how many transactions carry it and the rules that write it, each by file position with the transactions it wins. It is where near-duplicate tags and tags several rules feed show up, and a rule beaten by a more specific one reads (0) there as it does on the rules screen. A tag no rule names any more says the next retag clears it. The transfer label is not a tag and is never listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ const VIEWS = [
|
||||
{ id: 'rules', title: 'Rules', mount: mountRules },
|
||||
{ id: 'transfer', title: 'Transfer builder', mount: mountTransferBuilder },
|
||||
{ id: 'transfers', title: 'Transfers', mount: mountTransfers },
|
||||
{ id: 'tags', title: 'Tags', mount: mountTags },
|
||||
];
|
||||
|
||||
const state = {
|
||||
@@ -1049,6 +1050,42 @@ function mountTransfers(main) {
|
||||
return { refresh };
|
||||
}
|
||||
|
||||
// --- tags -----------------------------------------------------------------
|
||||
|
||||
// mountTags lists every distinct tag with how many transactions carry it and
|
||||
// the rules that write it, numbered by file position as on the rules screen.
|
||||
function mountTags(main) {
|
||||
const title = h('h2');
|
||||
const body = h('div');
|
||||
main.append(title, body);
|
||||
|
||||
async function refresh() {
|
||||
let tags;
|
||||
try {
|
||||
tags = (await api('GET', '/api/tags')).tags;
|
||||
} catch (e) { setError(e); return; }
|
||||
title.replaceChildren('Tags ', h('span', { class: 'sub' }, `· ${tags.length} tags`));
|
||||
if (!tags.length) {
|
||||
body.replaceChildren(h('div', { class: 'panel empty' }, 'No tags yet.\n\nA tag comes from a rule: build one on the rule builder.'));
|
||||
return;
|
||||
}
|
||||
body.replaceChildren(h('div', { class: 'panel' }, h('table', {},
|
||||
h('thead', {}, h('tr', {}, h('th', {}, 'Tag'), h('th', { class: 'num' }, 'Txns'), h('th', {}, 'Rules'))),
|
||||
h('tbody', {}, tags.map((t) => h('tr', {},
|
||||
h('td', { class: t.transactions ? null : 'muted' }, t.tag),
|
||||
h('td', { class: 'num' + (t.transactions ? '' : ' muted') }, t.transactions),
|
||||
h('td', { class: 'desc' }, t.rules.length
|
||||
? t.rules.map((r) => h('span', { class: 'tag-rule' + (r.usage ? '' : ' muted') },
|
||||
h('span', { class: 'muted' }, `#${r.pos + 1} `),
|
||||
h('span', { class: 'mono' }, r.pattern),
|
||||
r.account ? h('span', { class: 'muted' }, ` on ${r.account}`) : null,
|
||||
` (${r.usage})`))
|
||||
: h('span', { class: 'warn' }, 'no rule writes it — Retag clears it'))))))));
|
||||
}
|
||||
refresh();
|
||||
return { refresh };
|
||||
}
|
||||
|
||||
// --- keyboard -------------------------------------------------------------
|
||||
|
||||
// Single-key shortcuts, where they do not fight the browser. A builder is a
|
||||
|
||||
@@ -145,6 +145,8 @@ tr.total td { font-weight: 600; border-top: 1px solid var(--border); }
|
||||
tr.excluded td { color: var(--muted); }
|
||||
tr.excluded.first td { border-top: 1px dashed var(--border); }
|
||||
td.actions { text-align: right; }
|
||||
.mono { font-family: var(--mono); }
|
||||
.tag-rule { display: inline-block; margin-right: 14px; white-space: nowrap; }
|
||||
tr.legs > td { padding: 0 10px 10px 36px; background: var(--bg); }
|
||||
tr.legs:hover { background: none; }
|
||||
table.sub th { background: var(--bg); font-weight: 500; color: var(--muted); }
|
||||
|
||||
Reference in New Issue
Block a user