From d919c341b12ba1fcc2a3f9bc84b4e98160582990 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 12 Mar 2026 11:57:35 -0700 Subject: [PATCH] Fix: clear descendant selections in dropdown when parent toggled (#12326) --- .../filterable-dropdown.component.spec.ts | 53 +++++++++++++++++++ .../filterable-dropdown.component.ts | 35 ++++++++++++ 2 files changed, 88 insertions(+) diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts index 2ecf95f2b..1763239b1 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.spec.ts @@ -631,6 +631,59 @@ describe('FilterableDropdownComponent & FilterableDropdownSelectionModel', () => ]) }) + it('deselecting a parent clears selected descendants', () => { + const root: Tag = { id: 100, name: 'Root Tag' } + const child: Tag = { id: 101, name: 'Child Tag', parent: root.id } + const grandchild: Tag = { + id: 102, + name: 'Grandchild Tag', + parent: child.id, + } + const other: Tag = { id: 103, name: 'Other Tag' } + + selectionModel.items = [root, child, grandchild, other] + selectionModel.set(root.id, ToggleableItemState.Selected, false) + selectionModel.set(child.id, ToggleableItemState.Selected, false) + selectionModel.set(grandchild.id, ToggleableItemState.Selected, false) + selectionModel.set(other.id, ToggleableItemState.Selected, false) + + selectionModel.toggle(root.id, false) + + expect(selectionModel.getSelectedItems()).toEqual([other]) + }) + + it('un-excluding a parent clears excluded descendants', () => { + const root: Tag = { id: 110, name: 'Root Tag' } + const child: Tag = { id: 111, name: 'Child Tag', parent: root.id } + const other: Tag = { id: 112, name: 'Other Tag' } + + selectionModel.items = [root, child, other] + selectionModel.set(root.id, ToggleableItemState.Excluded, false) + selectionModel.set(child.id, ToggleableItemState.Excluded, false) + selectionModel.set(other.id, ToggleableItemState.Excluded, false) + + selectionModel.exclude(root.id, false) + + expect(selectionModel.getExcludedItems()).toEqual([other]) + }) + + it('excluding a selected parent clears selected descendants', () => { + const root: Tag = { id: 120, name: 'Root Tag' } + const child: Tag = { id: 121, name: 'Child Tag', parent: root.id } + const other: Tag = { id: 122, name: 'Other Tag' } + + selectionModel.manyToOne = true + selectionModel.items = [root, child, other] + selectionModel.set(root.id, ToggleableItemState.Selected, false) + selectionModel.set(child.id, ToggleableItemState.Selected, false) + selectionModel.set(other.id, ToggleableItemState.Selected, false) + + selectionModel.exclude(root.id, false) + + expect(selectionModel.getExcludedItems()).toEqual([root]) + expect(selectionModel.getSelectedItems()).toEqual([other]) + }) + it('resorts items immediately when document count sorting enabled', () => { const apple: Tag = { id: 55, name: 'Apple' } const zebra: Tag = { id: 56, name: 'Zebra' } diff --git a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts index ec5425630..bc15e3374 100644 --- a/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts +++ b/src-ui/src/app/components/common/filterable-dropdown/filterable-dropdown.component.ts @@ -231,6 +231,7 @@ export class FilterableDropdownSelectionModel { state == ToggleableItemState.Excluded ) { this.temporarySelectionStates.delete(id) + this.clearDescendantSelections(id) } if (!id) { @@ -257,6 +258,7 @@ export class FilterableDropdownSelectionModel { if (this.manyToOne || this.singleSelect) { this.temporarySelectionStates.set(id, ToggleableItemState.Excluded) + this.clearDescendantSelections(id) if (this.singleSelect) { for (let key of this.temporarySelectionStates.keys()) { @@ -277,9 +279,15 @@ export class FilterableDropdownSelectionModel { newState = ToggleableItemState.NotSelected } this.temporarySelectionStates.set(id, newState) + if (newState == ToggleableItemState.Excluded) { + this.clearDescendantSelections(id) + } } } else if (!id || state == ToggleableItemState.Excluded) { this.temporarySelectionStates.delete(id) + if (id) { + this.clearDescendantSelections(id) + } } if (fireEvent) { @@ -291,6 +299,33 @@ export class FilterableDropdownSelectionModel { return this.selectionStates.get(id) || ToggleableItemState.NotSelected } + private clearDescendantSelections(id: number) { + for (const descendantID of this.getDescendantIDs(id)) { + this.temporarySelectionStates.delete(descendantID) + } + } + + private getDescendantIDs(id: number): number[] { + const descendants: number[] = [] + const queue: number[] = [id] + + while (queue.length) { + const parentID = queue.shift() + for (const item of this._items) { + if ( + typeof item?.id === 'number' && + typeof (item as any)['parent'] === 'number' && + (item as any)['parent'] === parentID + ) { + descendants.push(item.id) + queue.push(item.id) + } + } + } + + return descendants + } + get logicalOperator(): LogicalOperator { return this.temporaryLogicalOperator }