fix(ui): keep filter row alive during table rebuild so typing never loses focus (rant 2026-08-25T11:15:16) - #148
Merged
Merged
Conversation
…oses focus (rant 2026-08-25T11:15:16) Column-filter inputs on the transactions page lost focus after every keystroke (regression of PR #146 server-side filters, v0.7.18): the debounced filter change triggered an async fetch, and the fetch-return render rebuilt the whole table via innerHTML, destroying the focused input. Focus-restore ran after the wrong render. Fix (host-confirmed option B): the header filter row is now rendered once and survives rebuilds — buildDataTable only rebuilds tbody and the pager; the input DOM is never destroyed so focus is naturally preserved. - split thead (sort buttons + filter row) into tableTheadHtml, rendered only when the container has no <table> yet (values seeded from state.filters; afterwards the inputs are the DOM source of truth) - tbody (tableBodyHtml) and pager rebuilt on every render as before - per-render event binding replaced with one-time container-level delegation (sort / filter input / pager / page-size / IME), reading the latest config via container._dt so events survive rebuilds - IME composition tracked (compositionstart/end + focusout safety): no table rebuild / no setSelectionRange while composing, refresh debounced once after compositionend — Chinese IME input is not interrupted - focus restore only as fallback when the input is not focused - cache-bust 20260825-2
13 tasks
argszero
added a commit
that referenced
this pull request
Sep 12, 2026
…age switches (#184) Setting a column filter on the transactions table and then switching the UI language emptied the visible table (0 rows and a blank empty-state) while #tx-count still reported the server's row count, because the filter state stored the localized option label while filterVal() re-localized on every comparison. The table header also stayed in the previous language: it is built once per container lifetime to preserve filter-input focus (#148), so column titles and dropdown labels never re-rendered. - TX_COLUMNS type/status options now emit {value, label}: value is the language-independent DB value (consume/earn/... , 成功/入账/处理中), label is the localized text. filterVal() compares against the raw row value. - Delete TX_TYPE_INV / TX_STATUS_INV: the "localized label -> DB value" inverse maps were keyed by runtime language output, so they could only express "translate whatever the dropdown currently shows". With the state holding the DB value they have nothing left to do. - tableTheadHtml renders both {value, label} and plain-string options. - Rebuild the header on atp:langchange so titles and options render in the newly selected language; the state (and therefore the user's filters) is preserved. Verified with a real-DOM A/B probe driving actual input events and I18n.setLang(): zh->en and en->zh flip from 0 rows + wrong-language header + blank empty-state to the correct rows and a re-localized header, while the zh->zh and en->en runs stay clean in both revisions. A separate guard confirms #148's focus-preserving property is intact (same node identity across typing and sort re-renders). Co-authored-by: argszero <argszero@argszerodeMac-mini.local>
11 tasks
argszero
added a commit
that referenced
this pull request
Sep 13, 2026
The transactions table's four Token columns (input / cached / output /
total) rendered a sortable header button, but clicking it never
reordered a row — while the Points and Time columns beside them sorted
correctly.
Two faces, one blind spot:
1. The comparator falls back to `row[key]`, and the view rows built by
`txsToView` do not carry those keys: the exact numbers live under
`inputRaw` / `cachedRaw` / `outputRaw` / `tokensRaw`, while `tokens`
and the three `*Tokens` fields hold the K/M *display* strings
("5K"). So `Number(undefined)` and `Number("5K")` are NaN, `cmp` is
NaN, `cmp !== 0` is false and the comparator degenerates to
"equal" — the rows never move and nothing is reported.
2. The `▲`/`▼` direction marker is emitted only by `tableTheadHtml`,
which since #148 runs only when the container has no `<table>`
(that is what keeps the filter inputs alive). At that moment
`state.sort` is still empty, so the sort state was never shown for
any column — including the Points column, which does sort.
The fix applies the rule C2054 already established for the Points
column (sort on the value the cell shows) to the class: each token
column declares a `sortVal` that reads its exact numeric field, and the
direction marker is painted in place on every rebuild instead of
relying on the one-shot thead build. Rebuilding the thead was
deliberately avoided — it would destroy the filter inputs and break
#148's focus invariant. `sortArrow()` and `colTitle()` are now the
single definition shared by the thead builder, the in-place painter and
`tableBodyHtml`'s `data-label`.
One file (ui/js/app.js), zero Rust, zero new i18n keys, no Config or
schema change.
11 tasks
argszero
added a commit
that referenced
this pull request
Sep 14, 2026
…th controls (#230) The transactions view filters by type through TWO controls — the `#tx-tabs` strip (全部/消费/收益) and the 类型 column's `select.th-filter` (the six database values) — but the state was split: the tabs wrote a module-level `txTab`, the select wrote `txTable.filters.type`, and `loadTransactions` resolved them with a documented precedence, `const type = colType || (txTab === "all" ? "" : txTab)`. The column select therefore wins permanently: once it has a value, a tab click sets `txTab` and repaints nothing else, so the click is a NO-OP (the request still carries the column filter). And because the highlight was painted from `txTab` alone, the strip meanwhile claims a filter that is not the applied one — with the 类型 select on 赠送, the list shows gift rows while 全部 stays lit. This is one filtered concept with one state that must be the single source for the request parameter, the tab highlight, and the select: the fix deletes the second state (`txTab`) and routes both controls through `txTypeFilter()` (read) / `setTxTypeFilter()` (write — it also syncs the already-rendered select, since #148 stopped rebuilding the thead and the widget no longer follows the state by itself). When the effective value is not one of all/consume/earn (topup / withdraw / gift / expire), no tab claims to be active: lighting 全部 while the list holds gift rows would be the same lie in the other direction. Verified with a jsdom probe (real ui/index.html + js/api.js + data.js + i18n.js + app.js, only `fetch` stubbed) that drives the real controls and compares all three artifacts against ONE derivation, `wantTabs(type)` — never a literal, which would pass by accident whenever the stale highlight happens to spell it. A/B (baseline pinned to d1faab6 via `git show`; the driver aborts if the baseline equals the fix): pre-change 5/11 (red B2/B3/B4/B5/C1/C2), three wrong fixes each rejected — dead click 4/11, unsynced select 9/11, constant highlight 7/11 — fixed tree 11/11 with the built-in control A0 green in every leg.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fix a regression from PR #146 (v0.7.18 server-side column filters): typing into a column-filter input on the transactions page lost focus after every keystroke, making it impossible to type a full word (e.g. "sh" — after "s" the input loses focus).
Root cause: input debounce callback →
renderTransactionsdetects the filter-signature change →loadTransactions()async fetch → after fetch returns,renderTransactions→buildDataTableexecutescontainer.innerHTML = html(full table rebuild) → the old filter input is destroyed, focus lost. The focus-restore code ran after the render that triggered the fetch, not after the DOM rebuild that follows the fetch return.Fix (host-confirmed option B): the header filter row is separated from the full-table rebuild —
buildDataTablenow renders the thead (sort buttons + filter row) only once per container and rebuilds only the tbody + pager on every render, so the filter input DOM is never destroyed and focus is naturally preserved.Related Issue
Rant
2026-08-25T11:15:16(verbatim):Changes
ui/js/app.js:tableTheadHtml()— thead (sort buttons + filter row) rendered once, only when the container has no<table>yet; input values seeded fromstate.filterson first render, afterwards the inputs are the DOM source of truthtableBodyHtml()— data rows, rebuilt on every renderbuildDataTable()— rebuilds only tbody + pager; container-level one-time event delegation (sort / filter input / pager / page-size / IME) reads the latest config viacontainer._dt, so events survive rebuilds and are never lost after the first keystrokecompositionstart/compositionend+focusoutsafety): no table rebuild and nosetSelectionRangewhile composing; a refresh is debounced aftercompositionend— Chinese IME composition is not interruptedui/index.html: cache-bust bumped to20260825-2Tests
node --check ui/js/app.js— OKcargo test— 147/147 passChecklist
fix/<description>main(default branch)