]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Fix: ensure custom field query propagation, change detection (#11291)
authorshamoon <4887959+shamoon@users.noreply.github.com>
Tue, 4 Nov 2025 20:40:05 +0000 (12:40 -0800)
committerGitHub <noreply@github.com>
Tue, 4 Nov 2025 20:40:05 +0000 (12:40 -0800)
src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.spec.ts
src-ui/src/app/components/common/custom-fields-query-dropdown/custom-fields-query-dropdown.component.ts
src-ui/src/app/utils/custom-field-query-element.spec.ts
src-ui/src/app/utils/custom-field-query-element.ts

index 4dcbceb13aa5ea2595aed4402bbabba873fcf91b..69f89f74e05410faead1a5cc491ae9201d1a1f72 100644 (file)
@@ -354,5 +354,13 @@ describe('CustomFieldsQueryDropdownComponent', () => {
       model.removeElement(atom)
       expect(completeSpy).toHaveBeenCalled()
     })
+
+    it('should subscribe to existing elements when queries are assigned', () => {
+      const expression = new CustomFieldQueryExpression()
+      const nextSpy = jest.spyOn(model.changed, 'next')
+      model.queries = [expression]
+      expression.changed.next(expression)
+      expect(nextSpy).toHaveBeenCalledWith(model)
+    })
   })
 })
index fc4e8ef199fb0fabf47e6c3957e779fa01d24fdb..7d8109c53edc241660d306cec19565aa2af742dd 100644 (file)
@@ -17,7 +17,7 @@ import {
 } from '@ng-bootstrap/ng-bootstrap'
 import { NgSelectComponent, NgSelectModule } from '@ng-select/ng-select'
 import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
-import { first, Subject, takeUntil } from 'rxjs'
+import { first, Subject, Subscription, takeUntil } from 'rxjs'
 import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
 import {
   CUSTOM_FIELD_QUERY_MAX_ATOMS,
@@ -41,10 +41,27 @@ import { ClearableBadgeComponent } from '../clearable-badge/clearable-badge.comp
 import { DocumentLinkComponent } from '../input/document-link/document-link.component'
 
 export class CustomFieldQueriesModel {
-  public queries: CustomFieldQueryElement[] = []
+  private _queries: CustomFieldQueryElement[] = []
+  private rootSubscriptions: Subscription[] = []
 
   public readonly changed = new Subject<CustomFieldQueriesModel>()
 
+  public get queries(): CustomFieldQueryElement[] {
+    return this._queries
+  }
+
+  public set queries(value: CustomFieldQueryElement[]) {
+    this.teardownRootSubscriptions()
+    this._queries = value ?? []
+    for (const element of this._queries) {
+      this.rootSubscriptions.push(
+        element.changed.subscribe(() => {
+          this.changed.next(this)
+        })
+      )
+    }
+  }
+
   public clear(fireEvent = true) {
     this.queries = []
     if (fireEvent) {
@@ -107,14 +124,14 @@ export class CustomFieldQueriesModel {
   public addExpression(
     expression: CustomFieldQueryExpression = new CustomFieldQueryExpression()
   ) {
-    if (this.queries.length > 0) {
-      ;(
-        (this.queries[0] as CustomFieldQueryExpression)
-          .value as CustomFieldQueryElement[]
-      ).push(expression)
-    } else {
-      this.queries.push(expression)
+    if (this.queries.length === 0) {
+      this.queries = [expression]
+      return
     }
+    ;(
+      (this.queries[0] as CustomFieldQueryExpression)
+        .value as CustomFieldQueryElement[]
+    ).push(expression)
     expression.changed.subscribe(() => {
       this.changed.next(this)
     })
@@ -166,6 +183,13 @@ export class CustomFieldQueriesModel {
       this.changed.next(this)
     }
   }
+
+  private teardownRootSubscriptions() {
+    for (const subscription of this.rootSubscriptions) {
+      subscription.unsubscribe()
+    }
+    this.rootSubscriptions = []
+  }
 }
 
 @Component({
index 411dcd6f92519bbbd285febdd549a7431ca8a8a0..e01af7fd43fa00ee49ca2542cd66825a81e50b23 100644 (file)
@@ -1,4 +1,3 @@
-import { fakeAsync, tick } from '@angular/core/testing'
 import {
   CustomFieldQueryElementType,
   CustomFieldQueryLogicalOperator,
@@ -111,13 +110,38 @@ describe('CustomFieldQueryAtom', () => {
     expect(atom.serialize()).toEqual([1, 'operator', 'value'])
   })
 
-  it('should emit changed on value change after debounce', fakeAsync(() => {
+  it('should emit changed on value change immediately', () => {
     const atom = new CustomFieldQueryAtom()
     const changeSpy = jest.spyOn(atom.changed, 'next')
     atom.value = 'new value'
-    tick(1000)
     expect(changeSpy).toHaveBeenCalled()
-  }))
+  })
+
+  it('should ignore duplicate array emissions', () => {
+    const atom = new CustomFieldQueryAtom()
+    atom.operator = CustomFieldQueryOperator.In
+    const changeSpy = jest.fn()
+    atom.changed.subscribe(changeSpy)
+
+    atom.value = [1, 2]
+    expect(changeSpy).toHaveBeenCalledTimes(1)
+
+    changeSpy.mockClear()
+    atom.value = [1, 2]
+    expect(changeSpy).not.toHaveBeenCalled()
+  })
+
+  it('should emit when array values differ while length matches', () => {
+    const atom = new CustomFieldQueryAtom()
+    atom.operator = CustomFieldQueryOperator.In
+    const changeSpy = jest.fn()
+    atom.changed.subscribe(changeSpy)
+
+    atom.value = [1, 2]
+    changeSpy.mockClear()
+    atom.value = [1, 3]
+    expect(changeSpy).toHaveBeenCalledTimes(1)
+  })
 })
 
 describe('CustomFieldQueryExpression', () => {
index 3438f2c85a6fff7bf5a91911aa65068752779743..34891641a6d84006e41910ca544c889540a5b6ac 100644 (file)
@@ -1,4 +1,4 @@
-import { Subject, debounceTime, distinctUntilChanged } from 'rxjs'
+import { Subject, distinctUntilChanged } from 'rxjs'
 import { v4 as uuidv4 } from 'uuid'
 import {
   CUSTOM_FIELD_QUERY_VALUE_TYPES_BY_OPERATOR,
@@ -110,7 +110,22 @@ export class CustomFieldQueryAtom extends CustomFieldQueryElement {
 
   protected override connectValueModelChanged(): void {
     this.valueModelChanged
-      .pipe(debounceTime(1000), distinctUntilChanged())
+      .pipe(
+        distinctUntilChanged((previous, current) => {
+          if (Array.isArray(previous) && Array.isArray(current)) {
+            if (previous.length !== current.length) {
+              return false
+            }
+            for (let i = 0; i < previous.length; i++) {
+              if (previous[i] !== current[i]) {
+                return false
+              }
+            }
+            return true
+          }
+          return previous === current
+        })
+      )
       .subscribe(() => {
         this.changed.next(this)
       })