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 } diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html index b154324c7..b3a29aed4 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.html @@ -15,7 +15,7 @@ } @if (document && displayFields?.includes(DisplayField.TAGS)) { -
+
@for (tagID of tagIDs; track tagID) { } diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss index 508c5251a..dce77802e 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.scss @@ -72,4 +72,14 @@ a { max-width: 80%; row-gap: .2rem; line-height: 1; + + &.tags-no-wrap { + ::ng-deep .badge { + display: inline-block; + max-width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } } diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts index 63cfc5a50..982b3980b 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.spec.ts @@ -82,6 +82,16 @@ describe('DocumentCardSmallComponent', () => { ).toHaveLength(6) }) + it('should clear hidden tag counter when tag count falls below the limit', () => { + expect(component.moreTags).toEqual(3) + + component.document.tags = [1, 2, 3, 4, 5, 6] + fixture.detectChanges() + + expect(component.moreTags).toBeNull() + expect(fixture.nativeElement.textContent).not.toContain('+ 3') + }) + it('should try to close the preview on mouse leave', () => { component.popupPreview = { close: jest.fn(), diff --git a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts index ad428dfab..05f84d752 100644 --- a/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts +++ b/src-ui/src/app/components/document-list/document-card-small/document-card-small.component.ts @@ -126,6 +126,7 @@ export class DocumentCardSmallComponent this.moreTags = this.document.tags.length - (limit - 1) return this.document.tags.slice(0, limit - 1) } else { + this.moreTags = null return this.document.tags } } diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss index eacc3b4e7..e6a4ea113 100644 --- a/src-ui/src/theme.scss +++ b/src-ui/src/theme.scss @@ -149,6 +149,10 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,