diff --git a/docs/changelog.md b/docs/changelog.md
index 9a1d3ef7f..e791ddf17 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -1,5 +1,29 @@
# Changelog
+## paperless-ngx 2.20.11
+
+### Security
+
+- Resolve [GHSA-59xh-5vwx-4c4q](https://github.com/paperless-ngx/paperless-ngx/security/advisories/GHSA-59xh-5vwx-4c4q)
+
+### Bug Fixes
+
+- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328))
+- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326))
+- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302))
+- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289))
+
+### All App Changes
+
+
+4 changes
+
+- Fix: correct dropdown list active color in dark mode [@shamoon](https://github.com/shamoon) ([#12328](https://github.com/paperless-ngx/paperless-ngx/pull/12328))
+- Fixhancement: clear descendant selections in dropdown when parent toggled [@shamoon](https://github.com/shamoon) ([#12326](https://github.com/paperless-ngx/paperless-ngx/pull/12326))
+- Fix: prevent wrapping with larger amounts of tags on small cards, reset moreTags setting to correct count [@shamoon](https://github.com/shamoon) ([#12302](https://github.com/paperless-ngx/paperless-ngx/pull/12302))
+- Fix: prevent stale db filename during workflow actions [@shamoon](https://github.com/shamoon) ([#12289](https://github.com/paperless-ngx/paperless-ngx/pull/12289))
+
+
## paperless-ngx 2.20.10
### Bug Fixes
diff --git a/pyproject.toml b/pyproject.toml
index 1240805e5..a62ad51fa 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "paperless-ngx"
-version = "2.20.10"
+version = "2.20.11"
description = "A community-supported supercharged document management system: scan, index and archive all your physical documents"
readme = "README.md"
requires-python = ">=3.11"
diff --git a/src-ui/package.json b/src-ui/package.json
index a1cfaf7d3..72784e86f 100644
--- a/src-ui/package.json
+++ b/src-ui/package.json
@@ -1,6 +1,6 @@
{
"name": "paperless-ngx-ui",
- "version": "2.20.10",
+ "version": "2.20.11",
"scripts": {
"preinstall": "npx only-allow pnpm",
"ng": "ng",
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 9dc5f019f..c8a536eab 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 f5b6ba89c..7a2274898 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
@@ -235,6 +235,7 @@ export class FilterableDropdownSelectionModel {
state == ToggleableItemState.Excluded
) {
this.temporarySelectionStates.delete(id)
+ this.clearDescendantSelections(id)
}
if (!id) {
@@ -261,6 +262,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()) {
@@ -281,9 +283,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) {
@@ -295,6 +303,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)) {
-
+
3">
@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/environments/environment.prod.ts b/src-ui/src/environments/environment.prod.ts
index a5bf6942f..1e8adbc22 100644
--- a/src-ui/src/environments/environment.prod.ts
+++ b/src-ui/src/environments/environment.prod.ts
@@ -6,7 +6,7 @@ export const environment = {
apiVersion: '10', // match src/paperless/settings.py
appTitle: 'Paperless-ngx',
tag: 'prod',
- version: '2.20.10',
+ version: '2.20.11',
webSocketHost: window.location.host,
webSocketProtocol: window.location.protocol == 'https:' ? 'wss:' : 'ws:',
webSocketBaseUrl: base_url.pathname + 'ws/',
diff --git a/src-ui/src/theme.scss b/src-ui/src/theme.scss
index 6ff5f4a09..c60284c8a 100644
--- a/src-ui/src/theme.scss
+++ b/src-ui/src/theme.scss
@@ -150,6 +150,10 @@ $form-check-radio-checked-bg-image-dark: url("data:image/svg+xml,