mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-28 08:49:38 +02:00
feat(cli): enforce required database selection and improve tree-picker UX (#86)
* feat(cli): enforce required database selection and improve tree-picker UX - Require at least one database driver via prompt `required: true` instead of looping on empty selection; remove the now-dead retry/back-on-empty branch. - Surface the recommended option with a "(recommended)" hint in the depth and query-history prompts. - Tree picker: add `◧` partial glyph for parents whose descendants are checked, and make `a` toggle select-all-visible / select-none. * fix(cli): drop unused export from tree-picker toggleSelectAllVisible Knip flagged the export as unused; the function is only consumed by the internal reducer via the 'toggle-select-all-visible' command, so demote it to a module-local helper to keep CI's dead-code check green. * test(cli): drop empty-selection warning assertion from setup test The empty-selection retry/warning loop in `chooseDrivers` was removed in favor of `multiselect`'s `required: true`, so the legacy warning string is unreachable. Update the test to assert the simpler back-from-selection return-to-embeddings flow.
This commit is contained in:
parent
e28b10454a
commit
6c4623f2ff
8 changed files with 146 additions and 60 deletions
|
|
@ -6,6 +6,7 @@ import {
|
|||
clearExpiredTransientHint,
|
||||
filterTree,
|
||||
flattenSelection,
|
||||
hasPartialChildren,
|
||||
moveCursor,
|
||||
reducer,
|
||||
selectAllVisible,
|
||||
|
|
@ -134,6 +135,54 @@ describe('selection invariants', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('hasPartialChildren', () => {
|
||||
const ROOT = 'r0000000-0000-0000-0000-000000000000';
|
||||
const CHILD_A = 'a0000000-0000-0000-0000-000000000000';
|
||||
const CHILD_B = 'b0000000-0000-0000-0000-000000000000';
|
||||
const GRANDCHILD = 'g0000000-0000-0000-0000-000000000000';
|
||||
const LEAF = 'l0000000-0000-0000-0000-000000000000';
|
||||
|
||||
function tree() {
|
||||
return buildPickerTree([
|
||||
{ id: ROOT, title: 'Root', parentId: null },
|
||||
{ id: CHILD_A, title: 'Child A', parentId: ROOT },
|
||||
{ id: CHILD_B, title: 'Child B', parentId: ROOT },
|
||||
{ id: GRANDCHILD, title: 'Grandchild', parentId: CHILD_A },
|
||||
{ id: LEAF, title: 'Leaf', parentId: null },
|
||||
]);
|
||||
}
|
||||
|
||||
function byId() {
|
||||
return new Map(tree().map((node) => [node.id, node]));
|
||||
}
|
||||
|
||||
it('returns false for a leaf node with no children', () => {
|
||||
expect(hasPartialChildren(LEAF, new Set(), byId())).toBe(false);
|
||||
expect(hasPartialChildren(LEAF, new Set([LEAF]), byId())).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when the node itself is checked', () => {
|
||||
expect(hasPartialChildren(ROOT, new Set([ROOT]), byId())).toBe(false);
|
||||
});
|
||||
|
||||
it('returns false when an ancestor is checked (node is locked)', () => {
|
||||
expect(hasPartialChildren(CHILD_A, new Set([ROOT]), byId())).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true when a direct child is checked', () => {
|
||||
expect(hasPartialChildren(ROOT, new Set([CHILD_A]), byId())).toBe(true);
|
||||
});
|
||||
|
||||
it('returns true when a deep descendant is checked', () => {
|
||||
expect(hasPartialChildren(ROOT, new Set([GRANDCHILD]), byId())).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false when no descendant is checked', () => {
|
||||
expect(hasPartialChildren(ROOT, new Set([LEAF]), byId())).toBe(false);
|
||||
expect(hasPartialChildren(ROOT, new Set(), byId())).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search and cursor movement', () => {
|
||||
it('filters by title and path while deriving auto-expanded ancestors', () => {
|
||||
const state = buildInitialState({
|
||||
|
|
@ -188,6 +237,23 @@ describe('bulk actions and reducer effects', () => {
|
|||
expect([...selectNone(selected).checked]).toEqual([]);
|
||||
});
|
||||
|
||||
it('toggle-select-all-visible selects all visible roots, then clears on repeat', () => {
|
||||
const state = buildInitialState({
|
||||
tree: buildPickerTree(pages()),
|
||||
existingSelectedIds: [],
|
||||
});
|
||||
|
||||
const firstPress = reducer(state, 'toggle-select-all-visible').next;
|
||||
expect(firstPress.checked.size).toBeGreaterThan(0);
|
||||
const allSelectedIds = [...firstPress.checked].sort();
|
||||
|
||||
const secondPress = reducer(firstPress, 'toggle-select-all-visible').next;
|
||||
expect([...secondPress.checked]).toEqual([]);
|
||||
|
||||
const thirdPress = reducer(secondPress, 'toggle-select-all-visible').next;
|
||||
expect([...thirdPress.checked].sort()).toEqual(allSelectedIds);
|
||||
});
|
||||
|
||||
it('saves immediately when confirm is not required and prompts confirmation when requireConfirmOnSave is true', () => {
|
||||
const noConfirm = toggleChecked(
|
||||
buildInitialState({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue