Refs #423
- parentOptions.ts: say that this list is the only guard today (updateCategory
checks neither depth nor cycle, #393 will), instead of pointing to a
service-side validation that does not exist yet. The height walk is also
bounded by the number of categories, so a missing cycle guard gives a wrong
height instead of an endless loop; a test edits a member of a cycle.
- AddKeywordDialog.tsx: drop `categoriesLoaded`, which only repeated what the
null initial category already guarantees.
- Tests: the form and the detail panel are pinned to what they hand to
buildParentOptions (editingId, currentParentId), through a pass-through spy.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Refs #423
The review of PR #428 showed that the rule only looked at direct children:
editing a root that carries grandchildren still offered the other roots as
a parent, and saving created a fourth level, which nothing guards against
in updateCategory or moveCategory. The allowed parent depth is now
MAX_PARENT_DEPTH minus the height of the edited sub-tree (0, 1 or 2), so a
category with grandchildren gets no parent at all, only its current one.
Test written first on the reviewer's probe ([1, 10->1, 100->10, 2],
editing 1: [] expected, [2] obtained before the fix).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Fixes#423
The "Parent category" field of CategoryForm and the "Category" field of
AddKeywordDialog were native selects with no typing. Both now use
CategoryCombobox, like the category cell of the Transactions page: typed
letters filter the list, which is ordered hierarchically with indentation
and shows seed categories through their i18n_key.
- CategoryCombobox accepts a minimal structural type (CategoryLike), so
CategoryTreeNode rows pass without conversion; sortHierarchical is
generic over it. Callers are unchanged.
- buildParentOptions (pure, tested) keeps roots and level-1 categories,
leaves out the edited category, and keeps only roots when the edited
category has children. That rule alone also leaves out its descent (a
descendant is never a root), so no separate subtree walk is needed. The
parent currently stored is always kept so the field never looks blank.
CategoryForm gets an editingId prop, passed by CategoryDetailPanel.
- AddKeywordDialog reads the Transactions list (active and inputable,
decision D2) through loadKeywordCategories, keeps the pre-filled
category only when it is in that list (reconcileInitialCategory), and
holds the apply button until the list is in. Its two fields sit above
the scrolling block of matches, so the open list is not clipped by the
scrollport.
- Tests: parentOptions, keywordCategory, and static renders of both
components. Changelogs and the user guide (with its i18n mirror).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>