Drop the keyboard shortcuts; add a Re-read all button
The page carried the TUI's keymap -- screens on 1-8, / u a i r, the report's arrows and s, the rule builder's ctrl+s and esc. Every one of them already had a link, button or click, so they are gone, along with the key numbers in the nav and the key hints in placeholders and tooltips. The upload box still answers Enter and Space, which is what makes it a button rather than a shortcut. The forced import was reachable only by shift-clicking Import. It is now a Re-read all button: every statement parsed again, including the ones an import skips as unchanged -- what to run after a parser fix. Both buttons disable while either runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -174,12 +174,12 @@ tag so ties keep a fixed position instead of shuffling between reloads.
|
|||||||
own copy of that mapping; a new order needs a column of its own, which
|
own copy of that mapping; a new order needs a column of its own, which
|
||||||
`TestOrderColumnsAreDistinct` checks.
|
`TestOrderColumnsAreDistinct` checks.
|
||||||
|
|
||||||
**The single-key shortcuts must not fire while typing.** The page's `keydown`
|
**There are no keyboard shortcuts.** The page used to carry the TUI's keymap —
|
||||||
handler ignores them whenever the target is an input, or typing `i` in the tag
|
screens on `1`–`8`, `u`, `/`, `i`, `r`, and chords inside the builders — and it
|
||||||
field would start an import. A view's own `key` hook runs first and is told
|
was removed on purpose: every action is a link or a button. Do not add a global
|
||||||
whether the user is typing, so a command *inside* a builder has to be a chord —
|
`keydown` handler back without being asked; a single-letter key fires while
|
||||||
`ctrl+s` re-sorts the preview — because every printable key belongs to the
|
typing in a field unless every one of them is guarded, which is what the old one
|
||||||
field being typed in.
|
existed to do.
|
||||||
|
|
||||||
**A rule's usage count is how many transactions it wins, not how many its glob
|
**A rule's usage count is how many transactions it wins, not how many its glob
|
||||||
could match** — `Engine.Usage` counts by `MatchIndex`, so a rule shadowed by a
|
could match** — `Engine.Usage` counts by `MatchIndex`, so a rule shadowed by a
|
||||||
|
|||||||
@@ -123,9 +123,8 @@ LIDL SOFIA 4412
|
|||||||
|
|
||||||
The account is not part of what makes a row distinct. A rule matches on the
|
The account is not part of what makes a row distinct. A rule matches on the
|
||||||
description and only optionally narrows to an account, so one payee seen on two
|
description and only optionally narrows to an account, so one payee seen on two
|
||||||
accounts is still one pattern to write — and it is one row in the rule builder
|
accounts is still one pattern to write — and it is one row in the rule builder,
|
||||||
on `4`, which groups the same way. Pass `--account` to scope the listing
|
which groups the same way. Pass `--account` to scope the listing instead.
|
||||||
instead.
|
|
||||||
|
|
||||||
It composes with the other filters (`--account`, `--month`, `--search`), and
|
It composes with the other filters (`--account`, `--month`, `--search`), and
|
||||||
`--limit` caps the rows printed, saying how many it held back. It cannot be
|
`--limit` caps the rows printed, saying how many it held back. It cannot be
|
||||||
@@ -164,11 +163,14 @@ fresh command would:
|
|||||||
rule there (another tab, a hand edit) the change is refused and you are asked
|
rule there (another tab, a hand edit) the change is refused and you are asked
|
||||||
to reload, rather than editing whichever rule moved into its place.
|
to reload, rather than editing whichever rule moved into its place.
|
||||||
|
|
||||||
Shift-click **Import** for `import --force`.
|
**Re-read all** is `import --force`: every statement is parsed again, including
|
||||||
|
the ones an Import skips because they have not changed. Use it after updating
|
||||||
|
money, when a parser has learned to read something it used to get wrong; rows
|
||||||
|
already in the index are recognised and not duplicated.
|
||||||
|
|
||||||
### Statements
|
### Statements
|
||||||
|
|
||||||
The Accounts screen (`1`) also lists every statement file in every account
|
The Accounts screen also lists every statement file in every account
|
||||||
folder, with what the index made of it:
|
folder, with what the index made of it:
|
||||||
|
|
||||||
| Status | Meaning |
|
| Status | Meaning |
|
||||||
@@ -200,7 +202,7 @@ button says **Forget** and only clears the index.
|
|||||||
|
|
||||||
### Adding statements from the browser
|
### Adding statements from the browser
|
||||||
|
|
||||||
The Accounts screen (`1`) has an **Add statements** panel: pick the account,
|
The Accounts screen has an **Add statements** panel: pick the account,
|
||||||
drop files on it (or click to choose them) and press **Upload**. The files are
|
drop files on it (or click to choose them) and press **Upload**. The files are
|
||||||
saved into that account's folder, exactly where you would have copied them by
|
saved into that account's folder, exactly where you would have copied them by
|
||||||
hand — the folder stays the source of truth, so deleting `index.db` and
|
hand — the folder stays the source of truth, so deleting `index.db` and
|
||||||
@@ -228,28 +230,18 @@ same name (a reissue) is numbered `_2`, `_3` rather than refused. A statement
|
|||||||
with no date it can find keeps its own name. Only uploads are named — files
|
with no date it can find keeps its own name. Only uploads are named — files
|
||||||
already in a folder are never renamed, since the index records them by path.
|
already in a folder are never renamed, since the index records them by path.
|
||||||
|
|
||||||
### Keys
|
### Navigating
|
||||||
|
|
||||||
| Key | Action |
|
Every screen is a link in the bar along the top, and every action is a button or
|
||||||
| --- | --- |
|
a click; there are no keyboard shortcuts. Clicking an account on the Accounts
|
||||||
| `1` – `8` | accounts · transactions · report · rule builder · rules · transfer builder · transfers · tags |
|
screen filters the transaction list to it.
|
||||||
| `/` | filter by description |
|
|
||||||
| `u` | show only untagged transactions (matched transfer legs are not among them) |
|
|
||||||
| `a` | clear the account filter |
|
|
||||||
| `←` `→` | move the report's period (report) |
|
|
||||||
| `s` | change how the report is sorted (report) |
|
|
||||||
| `ctrl+s` | sort the rule builder's preview by name / by count |
|
|
||||||
| `i` | import · `r` retag |
|
|
||||||
|
|
||||||
Inside a form every printable key belongs to the field you are typing in, so
|
|
||||||
the single-letter keys only work outside one; that is why the builder's sort is
|
|
||||||
a chord. Clicking an account on `1` filters the transaction list to it.
|
|
||||||
|
|
||||||
There is no control that tags a transaction. Tags come from `rules.toml` and
|
There is no control that tags a transaction. Tags come from `rules.toml` and
|
||||||
nowhere else, so tagging what you are looking at means writing a rule for it on
|
nowhere else, so tagging what you are looking at means writing a rule for it in
|
||||||
`4` — which is why that screen shows you what a glob catches before you save.
|
the rule builder — which is why it shows you what a glob catches before you
|
||||||
|
save.
|
||||||
|
|
||||||
### Report (`3`)
|
### Report
|
||||||
|
|
||||||
The report opens on **last month**, not on everything you have ever imported:
|
The report opens on **last month**, not on everything you have ever imported:
|
||||||
an all-time total is the one number a spending report is least often asked for,
|
an all-time total is the one number a spending report is least often asked for,
|
||||||
@@ -259,8 +251,8 @@ period is empty rather than quietly showing you a different one.
|
|||||||
|
|
||||||
The time axis beside the totals runs from the widest window down to the oldest
|
The time axis beside the totals runs from the widest window down to the oldest
|
||||||
month in the index — `all time`, `this year`, `last 12 months`, `last 3
|
month in the index — `all time`, `this year`, `last 12 months`, `last 3
|
||||||
months`, `this month`, `last month`, then one entry per month. Click one, or
|
months`, `this month`, `last month`, then one entry per month. Click one to
|
||||||
step along it with `←` and `→`.
|
report on it.
|
||||||
|
|
||||||
The three rolling windows run to the end of *this* month rather than to the
|
The three rolling windows run to the end of *this* month rather than to the
|
||||||
last complete one — you ask for "last 3 months" to see what is happening now,
|
last complete one — you ask for "last 3 months" to see what is happening now,
|
||||||
@@ -268,12 +260,11 @@ and leaving out the days since the 1st would answer a different question. Each
|
|||||||
month below `last month` is a month the index actually holds; months that a
|
month below `last month` is a month the index actually holds; months that a
|
||||||
named window above already covers are not repeated.
|
named window above already covers are not repeated.
|
||||||
|
|
||||||
Click a column heading, or press `s` to cycle, to change how the rows are
|
Click a column heading to change how the rows are arranged; the marked heading
|
||||||
arranged; the marked heading says which one they are arranged by: `Out ▾`
|
says which one they are arranged by: `Out ▾` largest spend first (the default
|
||||||
largest spend first (the default question a spending report answers), then
|
question a spending report answers), then `In ▾`, `Net ▾` lowest first so the
|
||||||
`In ▾`, `Net ▾` lowest first so the biggest losses lead, `N ▾` most
|
biggest losses lead, `N ▾` most transactions first, and `Tag ▴` A→Z. `money
|
||||||
transactions first, and `Tag ▴` A→Z. `money report` takes the same choice as
|
report` takes the same choice as `--sort out|in|net|count|tag`.
|
||||||
`--sort out|in|net|count|tag`.
|
|
||||||
|
|
||||||
Currency is always the outer grouping and no sort changes that — there are no
|
Currency is always the outer grouping and no sort changes that — there are no
|
||||||
exchange rates here, so two currencies interleaved by amount would invite a
|
exchange rates here, so two currencies interleaved by amount would invite a
|
||||||
@@ -283,12 +274,12 @@ The sort rearranges rows; it never changes which rows there are.
|
|||||||
|
|
||||||
The period narrows the report and only the report. The account, the search and
|
The period narrows the report and only the report. The account, the search and
|
||||||
the untagged toggle are shared with the transaction list, so opening on last
|
the untagged toggle are shared with the transaction list, so opening on last
|
||||||
month does not hide the rest of the index from `2`.
|
month does not hide the rest of the index from the transaction list.
|
||||||
|
|
||||||
`money report` takes `--month` instead; there is no command-line equivalent of
|
`money report` takes `--month` instead; there is no command-line equivalent of
|
||||||
the wider windows.
|
the wider windows.
|
||||||
|
|
||||||
### Rule builder (`4`)
|
### Rule builder
|
||||||
|
|
||||||
Writing rules by hand means guessing what a glob will catch. This screen shows
|
Writing rules by hand means guessing what a glob will catch. This screen shows
|
||||||
the answer as you type: the form is on the left — glob, account, tag, note —
|
the answer as you type: the form is on the left — glob, account, tag, note —
|
||||||
@@ -310,13 +301,13 @@ point to narrow down.
|
|||||||
immediately, so the rows it caught disappear from the list. The account stays
|
immediately, so the rows it caught disappear from the list. The account stays
|
||||||
filled in, since the next rule is usually for the same one.
|
filled in, since the next rule is usually for the same one.
|
||||||
|
|
||||||
Clicking the column headings, or `ctrl+s`, switches the list between by name and
|
Clicking the column headings switches the list between by name and by count,
|
||||||
by count, most seen first; the `↓` says which column the order is read from.
|
most seen first; the `↓` says which column the order is read from. The two
|
||||||
The two answer different questions: by name finds the payee you are looking at,
|
answer different questions: by name finds the payee you are looking at, by count
|
||||||
by count finds the rule worth writing next, since one pattern claiming forty
|
finds the rule worth writing next, since one pattern claiming forty rows is
|
||||||
rows is worth more than the first of forty claiming one. Ties keep the
|
worth more than the first of forty claiming one. Ties keep the alphabetical
|
||||||
alphabetical order, so the list does not reshuffle under you, and the choice
|
order, so the list does not reshuffle under you, and the choice lasts until you
|
||||||
lasts until you change it.
|
change it.
|
||||||
|
|
||||||
The account and tag fields offer completions as you type. Accounts come from
|
The account and tag fields offer completions as you type. Accounts come from
|
||||||
the folders on disk and the index; tags from every tag in use plus any named in
|
the folders on disk and the index; tags from every tag in use plus any named in
|
||||||
@@ -336,7 +327,7 @@ report leaves them out anyway.
|
|||||||
|
|
||||||
#### Editing a rule
|
#### Editing a rule
|
||||||
|
|
||||||
**Edit** on the rules screen (`5`) opens the rule in this same form with the
|
**Edit** on the rules screen opens the rule in this same form with the
|
||||||
fields filled in, and saving rewrites that rule where it sits instead of
|
fields filled in, and saving rewrites that rule where it sits instead of
|
||||||
appending a new one. It keeps its position, since position still breaks ties
|
appending a new one. It keeps its position, since position still breaks ties
|
||||||
between equally specific rules; `rules.toml` is edited textually, so the
|
between equally specific rules; `rules.toml` is edited textually, so the
|
||||||
@@ -352,10 +343,10 @@ you are about to hand back to whatever rule catches it next.
|
|||||||
|
|
||||||
The form has no `type` field, so a rule that sets one carries it through
|
The form has no `type` field, so a rule that sets one carries it through
|
||||||
unchanged rather than losing it; it is shown under the glob as `+ type:… ·
|
unchanged rather than losing it; it is shown under the glob as `+ type:… ·
|
||||||
kept`. Saving lands back on the rules screen with the new counts; **Cancel**,
|
kept`. Saving lands back on the rules screen with the new counts; **Cancel**, or
|
||||||
`esc`, or leaving the screen leaves the rule as it was.
|
leaving the screen leaves the rule as it was.
|
||||||
|
|
||||||
### Rules (`5`)
|
### Rules
|
||||||
|
|
||||||
Every rule in file order — the order they are written in, not the order they
|
Every rule in file order — the order they are written in, not the order they
|
||||||
are tried in — with the number of transactions it actually claims. Rules that
|
are tried in — with the number of transactions it actually claims. Rules that
|
||||||
@@ -390,7 +381,7 @@ it, while one separated by a blank line is treated as a section heading and
|
|||||||
left alone; an edited rule keeps its comments, since they say why it is there
|
left alone; an edited rule keeps its comments, since they say why it is there
|
||||||
and changing its glob rarely changes that.
|
and changing its glob rarely changes that.
|
||||||
|
|
||||||
### Transfer builder (`6`)
|
### Transfer builder
|
||||||
|
|
||||||
Same idea as the rule builder, for money moved between your own accounts. The
|
Same idea as the rule builder, for money moved between your own accounts. The
|
||||||
form names both sides — from account, from desc, to account, to desc — plus an
|
form names both sides — from account, from desc, to account, to desc — plus an
|
||||||
@@ -425,7 +416,7 @@ A half-written definition previews too: fill in one side and its legs show up as
|
|||||||
unpaired, which is the quickest way to see that a glob is wrong before you have
|
unpaired, which is the quickest way to see that a glob is wrong before you have
|
||||||
written the other half.
|
written the other half.
|
||||||
|
|
||||||
### Transfers (`7`)
|
### Transfers
|
||||||
|
|
||||||
Every definition in file order, with what it currently pairs.
|
Every definition in file order, with what it currently pairs.
|
||||||
|
|
||||||
@@ -471,7 +462,7 @@ once, both after asking. Definitions marked `⚠` are never pruned — they are
|
|||||||
doing something, just not finishing it, and deleting one would hide the problem
|
doing something, just not finishing it, and deleting one would hide the problem
|
||||||
rather than fix it. **Refresh pairing** re-reads what is currently in the index.
|
rather than fix it. **Refresh pairing** re-reads what is currently in the index.
|
||||||
|
|
||||||
### Tags (`8`)
|
### Tags
|
||||||
|
|
||||||
Every distinct tag, A→Z: the ones your transactions carry and any `rules.toml`
|
Every distinct tag, A→Z: the ones your transactions carry and any `rules.toml`
|
||||||
names, with how many transactions carry each and the rules that write it.
|
names, with how many transactions carry each and the rules that write it.
|
||||||
@@ -547,7 +538,7 @@ tag = "groceries"
|
|||||||
Every tag comes from this file, so a tag is never something you have to keep
|
Every tag comes from this file, so a tag is never something you have to keep
|
||||||
safe: `money retag` recomputes all of them from the current rules and is safe
|
safe: `money retag` recomputes all of them from the current rules and is safe
|
||||||
to run whenever you change it. A transaction no rule matches simply stays
|
to run whenever you change it. A transaction no rule matches simply stays
|
||||||
untagged, and shows up under `money ls --untagged`, the `u` view, and
|
untagged, and shows up under `money ls --untagged`, **Untagged only**, and
|
||||||
`(untagged)` in the report — unless it is one leg of a matched transfer, which
|
`(untagged)` in the report — unless it is one leg of a matched transfer, which
|
||||||
is already spoken for and is left out of all three.
|
is already spoken for and is left out of all three.
|
||||||
|
|
||||||
@@ -649,7 +640,7 @@ that is the number your spending is short by:
|
|||||||
transfers excluded: 2 legs in EUR, 500.00 out, 495.00 in, 5.00 in fees
|
transfers excluded: 2 legs in EUR, 500.00 out, 495.00 in, 5.00 in fees
|
||||||
```
|
```
|
||||||
|
|
||||||
The report screen (`3`) shows the same thing under its `TOTAL`, the fee on its
|
The report screen shows the same thing under its `TOTAL`, the fee on its
|
||||||
own row (for whichever period it is on):
|
own row (for whichever period it is on):
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -675,7 +666,7 @@ An *unpaired* leg is not a transfer and still counts, tagged like anything
|
|||||||
else. That is deliberate: money that left an account and cannot be shown to
|
else. That is deliberate: money that left an account and cannot be shown to
|
||||||
have arrived is exactly what you want to see, not something to quietly drop.
|
have arrived is exactly what you want to see, not something to quietly drop.
|
||||||
`money import` and `money retag` report unpaired legs on stderr, and the
|
`money import` and `money retag` report unpaired legs on stderr, and the
|
||||||
transfers screen (`7`) shows which definition they belong to.
|
transfers screen shows which definition they belong to.
|
||||||
|
|
||||||
The pairing is derived state, like the tags: it lives in the index, is
|
The pairing is derived state, like the tags: it lives in the index, is
|
||||||
recomputed wholesale by `money retag` and after every import, and disappears
|
recomputed wholesale by `money retag` and after every import, and disappears
|
||||||
|
|||||||
+22
-83
@@ -175,7 +175,7 @@ function renderNav() {
|
|||||||
nav.replaceChildren(...VIEWS.map((v, i) => h('a', {
|
nav.replaceChildren(...VIEWS.map((v, i) => h('a', {
|
||||||
href: '#' + v.id,
|
href: '#' + v.id,
|
||||||
class: v.id === state.view ? 'active' : null,
|
class: v.id === state.view ? 'active' : null,
|
||||||
}, h('kbd', {}, String(i + 1)), v.title)));
|
}, v.title)));
|
||||||
}
|
}
|
||||||
|
|
||||||
function go(id) {
|
function go(id) {
|
||||||
@@ -189,8 +189,8 @@ function go(id) {
|
|||||||
function show() {
|
function show() {
|
||||||
const id = location.hash.slice(1) || 'accounts';
|
const id = location.hash.slice(1) || 'accounts';
|
||||||
const view = VIEWS.find((v) => v.id === id) || VIEWS[0];
|
const view = VIEWS.find((v) => v.id === id) || VIEWS[0];
|
||||||
// Leaving the builder mid-edit abandons the edit, as esc does: a new rule
|
// Leaving the builder mid-edit abandons the edit, as Cancel does: a new
|
||||||
// must not start out as a copy of the one being edited.
|
// rule must not start out as a copy of the one being edited.
|
||||||
if (view.id !== 'rule' && state.rule.editing) state.rule = blankRule();
|
if (view.id !== 'rule' && state.rule.editing) state.rule = blankRule();
|
||||||
state.view = view.id;
|
state.view = view.id;
|
||||||
renderNav();
|
renderNav();
|
||||||
@@ -206,12 +206,16 @@ async function refreshAll() {
|
|||||||
|
|
||||||
// --- import and retag -----------------------------------------------------
|
// --- import and retag -----------------------------------------------------
|
||||||
|
|
||||||
|
// runImport is `money import`, or with force `money import --force`: every
|
||||||
|
// statement re-read, including the ones the checksum would skip.
|
||||||
async function runImport(force) {
|
async function runImport(force) {
|
||||||
if (state.busy) return;
|
if (state.busy) return;
|
||||||
state.busy = true;
|
state.busy = true;
|
||||||
const btn = document.getElementById('import');
|
const buttons = ['import', 'reread'].map((id) => document.getElementById(id));
|
||||||
btn.disabled = true;
|
const btn = buttons[force ? 1 : 0];
|
||||||
btn.textContent = 'Importing…';
|
const label = btn.textContent;
|
||||||
|
for (const b of buttons) b.disabled = true;
|
||||||
|
btn.textContent = force ? 'Re-reading…' : 'Importing…';
|
||||||
setStatus('importing… parsing PDF statements can take a while');
|
setStatus('importing… parsing PDF statements can take a while');
|
||||||
try {
|
try {
|
||||||
const res = await api('POST', '/api/import', { force: !!force });
|
const res = await api('POST', '/api/import', { force: !!force });
|
||||||
@@ -224,8 +228,8 @@ async function runImport(force) {
|
|||||||
setError(e);
|
setError(e);
|
||||||
} finally {
|
} finally {
|
||||||
state.busy = false;
|
state.busy = false;
|
||||||
btn.disabled = false;
|
for (const b of buttons) b.disabled = false;
|
||||||
btn.textContent = 'Import';
|
btn.textContent = label;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -459,7 +463,7 @@ function scopeToolbar(onChange) {
|
|||||||
select.value = state.filter.account;
|
select.value = state.filter.account;
|
||||||
|
|
||||||
const search = h('input', {
|
const search = h('input', {
|
||||||
type: 'search', id: 'search', placeholder: 'Filter by description ( / )',
|
type: 'search', id: 'search', placeholder: 'Filter by description',
|
||||||
value: state.filter.search,
|
value: state.filter.search,
|
||||||
oninput: debounce((e) => { state.filter.search = e.target.value.trim(); onChange(); }, 200),
|
oninput: debounce((e) => { state.filter.search = e.target.value.trim(); onChange(); }, 200),
|
||||||
});
|
});
|
||||||
@@ -468,7 +472,7 @@ function scopeToolbar(onChange) {
|
|||||||
onchange: (e) => { state.filter.untagged = e.target.checked; onChange(); },
|
onchange: (e) => { state.filter.untagged = e.target.checked; onChange(); },
|
||||||
});
|
});
|
||||||
return h('div', { class: 'toolbar' }, select, search,
|
return h('div', { class: 'toolbar' }, select, search,
|
||||||
h('label', { class: 'check', title: 'No tag and not a matched transfer leg (u)' }, untagged, 'Untagged only'));
|
h('label', { class: 'check', title: 'No tag and not a matched transfer leg' }, untagged, 'Untagged only'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- transactions ---------------------------------------------------------
|
// --- transactions ---------------------------------------------------------
|
||||||
@@ -571,7 +575,7 @@ function mountReport(main) {
|
|||||||
// An empty period is not an empty index, and the default window is the
|
// An empty period is not an empty index, and the default window is the
|
||||||
// one most likely to be empty: last month's statement may not be in yet.
|
// one most likely to be empty: last month's statement may not be in yet.
|
||||||
content = h('div', { class: 'panel empty' }, period.bounded && !data.indexEmpty
|
content = h('div', { class: 'panel empty' }, period.bounded && !data.indexEmpty
|
||||||
? `Nothing in ${period.name}.\n\nChoose another period (← →), or import.`
|
? `Nothing in ${period.name}.\n\nChoose another period, or import.`
|
||||||
: 'Nothing to report yet.\n\nImport some statements first.');
|
: 'Nothing to report yet.\n\nImport some statements first.');
|
||||||
} else {
|
} else {
|
||||||
const order = data.orders.find((o) => o.name === data.order);
|
const order = data.orders.find((o) => o.name === data.order);
|
||||||
@@ -611,21 +615,7 @@ function mountReport(main) {
|
|||||||
} catch (e) { setError(e); }
|
} catch (e) { setError(e); }
|
||||||
}
|
}
|
||||||
refresh();
|
refresh();
|
||||||
return {
|
return { refresh };
|
||||||
refresh,
|
|
||||||
key(e, typing) {
|
|
||||||
if (typing || !data || e.ctrlKey || e.metaKey || e.altKey) return false;
|
|
||||||
if (e.key === 'ArrowLeft') { setPeriod(data.period - 1); return true; }
|
|
||||||
if (e.key === 'ArrowRight') { setPeriod(data.period + 1); return true; }
|
|
||||||
if (e.key === 's') {
|
|
||||||
const i = data.orders.findIndex((o) => o.name === data.order);
|
|
||||||
state.report.sort = data.orders[(i + 1) % data.orders.length].name;
|
|
||||||
refresh();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- form helpers ---------------------------------------------------------
|
// --- form helpers ---------------------------------------------------------
|
||||||
@@ -712,8 +702,8 @@ function mountRuleBuilder(main) {
|
|||||||
}
|
}
|
||||||
preview.replaceChildren(h('table', {},
|
preview.replaceChildren(h('table', {},
|
||||||
h('thead', {}, h('tr', {}, h('th', {}, ''),
|
h('thead', {}, h('tr', {}, h('th', {}, ''),
|
||||||
h('th', { class: 'sortable', onclick: toggle, title: 'Sort by name / by count (ctrl+s)' }, descTitle),
|
h('th', { class: 'sortable', onclick: toggle, title: 'Sort by name / by count' }, descTitle),
|
||||||
h('th', { class: 'num sortable', onclick: toggle, title: 'Sort by name / by count (ctrl+s)' }, 'N' + (byCount ? ' ↓' : '')))),
|
h('th', { class: 'num sortable', onclick: toggle, title: 'Sort by name / by count' }, 'N' + (byCount ? ' ↓' : '')))),
|
||||||
h('tbody', {}, res.rows.map((row) => h('tr', {
|
h('tbody', {}, res.rows.map((row) => h('tr', {
|
||||||
class: 'clickable',
|
class: 'clickable',
|
||||||
title: 'Use this description as the glob',
|
title: 'Use this description as the glob',
|
||||||
@@ -758,23 +748,7 @@ function mountRuleBuilder(main) {
|
|||||||
|
|
||||||
inputs.match.focus();
|
inputs.match.focus();
|
||||||
refreshNow();
|
refreshNow();
|
||||||
return {
|
return { refresh: refreshNow };
|
||||||
refresh: refreshNow,
|
|
||||||
form: true,
|
|
||||||
key(e) {
|
|
||||||
if (e.key === 's' && e.ctrlKey) {
|
|
||||||
state.ruleSort = state.ruleSort === 'count' ? 'name' : 'count';
|
|
||||||
refreshNow();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (e.key === 'Escape' && editing) {
|
|
||||||
state.rule = blankRule();
|
|
||||||
go('rules');
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- rules ----------------------------------------------------------------
|
// --- rules ----------------------------------------------------------------
|
||||||
@@ -938,7 +912,7 @@ function mountTransferBuilder(main) {
|
|||||||
|
|
||||||
inputs.fromAccount.focus();
|
inputs.fromAccount.focus();
|
||||||
refreshNow();
|
refreshNow();
|
||||||
return { refresh: refreshNow, form: true };
|
return { refresh: refreshNow };
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- transfers ------------------------------------------------------------
|
// --- transfers ------------------------------------------------------------
|
||||||
@@ -1086,43 +1060,8 @@ function mountTags(main) {
|
|||||||
return { refresh };
|
return { refresh };
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- keyboard -------------------------------------------------------------
|
document.getElementById('import').addEventListener('click', () => runImport(false));
|
||||||
|
document.getElementById('reread').addEventListener('click', () => runImport(true));
|
||||||
// Single-key shortcuts, where they do not fight the browser. A builder is a
|
|
||||||
// form, so outside of chords its keys belong to the field being typed in.
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
const t = e.target;
|
|
||||||
const typing = t instanceof HTMLInputElement || t instanceof HTMLSelectElement || t instanceof HTMLTextAreaElement;
|
|
||||||
if (current && current.key && current.key(e, typing)) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (typing || e.ctrlKey || e.metaKey || e.altKey) return;
|
|
||||||
|
|
||||||
if (e.key >= '1' && e.key <= String(VIEWS.length)) {
|
|
||||||
go(VIEWS[Number(e.key) - 1].id);
|
|
||||||
} else if (e.key === '/') {
|
|
||||||
if (state.view !== 'txns' && state.view !== 'report') go('txns');
|
|
||||||
setTimeout(() => { const s = document.getElementById('search'); if (s) s.focus(); });
|
|
||||||
} else if (e.key === 'u') {
|
|
||||||
state.filter.untagged = !state.filter.untagged;
|
|
||||||
if (state.view === 'txns' || state.view === 'report') show();
|
|
||||||
else go('txns');
|
|
||||||
} else if (e.key === 'a') {
|
|
||||||
state.filter.account = '';
|
|
||||||
if (state.view === 'txns' || state.view === 'report') show();
|
|
||||||
} else if (e.key === 'i') {
|
|
||||||
runImport(false);
|
|
||||||
} else if (e.key === 'r') {
|
|
||||||
retag();
|
|
||||||
} else {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
e.preventDefault();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('import').addEventListener('click', (e) => runImport(e.shiftKey));
|
|
||||||
document.getElementById('import').title += ' (shift-click re-parses unchanged files too)';
|
|
||||||
document.getElementById('retag').addEventListener('click', retag);
|
document.getElementById('retag').addEventListener('click', retag);
|
||||||
window.addEventListener('hashchange', show);
|
window.addEventListener('hashchange', show);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<nav id="nav" aria-label="Screens"></nav>
|
<nav id="nav" aria-label="Screens"></nav>
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button type="button" id="retag" title="Re-apply rules.toml: retag everything and re-pair transfers">Retag</button>
|
<button type="button" id="retag" title="Re-apply rules.toml: retag everything and re-pair transfers">Retag</button>
|
||||||
|
<button type="button" id="reread" title="Re-read every statement, including ones unchanged since they were imported">Re-read all</button>
|
||||||
<button type="button" id="import" class="primary" title="Import new statements from every account folder">Import</button>
|
<button type="button" id="import" class="primary" title="Import new statements from every account folder">Import</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ button, input, select { font: inherit; color: inherit; }
|
|||||||
}
|
}
|
||||||
#nav a:hover { color: var(--text); background: var(--row-hover); }
|
#nav a:hover { color: var(--text); background: var(--row-hover); }
|
||||||
#nav a.active { color: var(--accent); background: var(--accent-soft); font-weight: 600; }
|
#nav a.active { color: var(--accent); background: var(--accent-soft); font-weight: 600; }
|
||||||
#nav kbd { font-family: var(--mono); font-size: 11px; opacity: .6; margin-right: 4px; }
|
|
||||||
.actions { display: flex; gap: 8px; }
|
.actions { display: flex; gap: 8px; }
|
||||||
|
|
||||||
button {
|
button {
|
||||||
|
|||||||
Reference in New Issue
Block a user