mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-11-03 03:16:10 -06:00 
			
		
		
		
	Enhancement: document link field fixes (#5020)
* Implement more efficient getFew for document retrieval * Filter out parent document ID & already-selected documents * Clip very long document titles
This commit is contained in:
		@@ -1,6 +1,10 @@
 | 
				
			|||||||
::ng-deep .ng-select-container .ng-value-container .ng-value {
 | 
					::ng-deep .ng-select-container .ng-value-container {
 | 
				
			||||||
    background-color: transparent !important;
 | 
					    overflow: hidden;
 | 
				
			||||||
    border-color: transparent;
 | 
					
 | 
				
			||||||
 | 
					    .ng-value {
 | 
				
			||||||
 | 
					        background-color: transparent !important;
 | 
				
			||||||
 | 
					        border-color: transparent;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.sidebaricon {
 | 
					.sidebaricon {
 | 
				
			||||||
@@ -9,6 +13,4 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
.badge {
 | 
					.badge {
 | 
				
			||||||
    font-size: .75rem;
 | 
					    font-size: .75rem;
 | 
				
			||||||
    // --bs-primary: var(--pngx-bg-alt);
 | 
					 | 
				
			||||||
    // color: var(--pngx-primary-text-contrast);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,10 @@ const documents = [
 | 
				
			|||||||
    id: 12,
 | 
					    id: 12,
 | 
				
			||||||
    title: 'Document 12 bar',
 | 
					    title: 'Document 12 bar',
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  {
 | 
				
			||||||
 | 
					    id: 16,
 | 
				
			||||||
 | 
					    title: 'Document 16 bar',
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    id: 23,
 | 
					    id: 23,
 | 
				
			||||||
    title: 'Document 23 bar',
 | 
					    title: 'Document 23 bar',
 | 
				
			||||||
@@ -48,10 +52,15 @@ describe('DocumentLinkComponent', () => {
 | 
				
			|||||||
    fixture.detectChanges()
 | 
					    fixture.detectChanges()
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should retrieve selected documents from APIs', () => {
 | 
					  it('should retrieve selected documents from API', () => {
 | 
				
			||||||
    const getSpy = jest.spyOn(documentService, 'getCachedMany')
 | 
					    const getSpy = jest.spyOn(documentService, 'getFew')
 | 
				
			||||||
    getSpy.mockImplementation((ids) => {
 | 
					    getSpy.mockImplementation((ids) => {
 | 
				
			||||||
      return of(documents.filter((d) => ids.includes(d.id)))
 | 
					      const docs = documents.filter((d) => ids.includes(d.id))
 | 
				
			||||||
 | 
					      return of({
 | 
				
			||||||
 | 
					        count: docs.length,
 | 
				
			||||||
 | 
					        all: docs.map((d) => d.id),
 | 
				
			||||||
 | 
					        results: docs,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    component.writeValue([1])
 | 
					    component.writeValue([1])
 | 
				
			||||||
    expect(getSpy).toHaveBeenCalled()
 | 
					    expect(getSpy).toHaveBeenCalled()
 | 
				
			||||||
@@ -85,12 +94,18 @@ describe('DocumentLinkComponent', () => {
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should load values correctly', () => {
 | 
					  it('should load values correctly', () => {
 | 
				
			||||||
    jest.spyOn(documentService, 'getCachedMany').mockImplementation((ids) => {
 | 
					    const getSpy = jest.spyOn(documentService, 'getFew')
 | 
				
			||||||
      return of(documents.filter((d) => ids.includes(d.id)))
 | 
					    getSpy.mockImplementation((ids) => {
 | 
				
			||||||
 | 
					      const docs = documents.filter((d) => ids.includes(d.id))
 | 
				
			||||||
 | 
					      return of({
 | 
				
			||||||
 | 
					        count: docs.length,
 | 
				
			||||||
 | 
					        all: docs.map((d) => d.id),
 | 
				
			||||||
 | 
					        results: docs,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    component.writeValue([12, 23])
 | 
					    component.writeValue([12, 23])
 | 
				
			||||||
    expect(component.value).toEqual([12, 23])
 | 
					    expect(component.value).toEqual([12, 23])
 | 
				
			||||||
    expect(component.selectedDocuments).toEqual([documents[1], documents[2]])
 | 
					    expect(component.selectedDocuments).toEqual([documents[1], documents[3]])
 | 
				
			||||||
    component.writeValue(null)
 | 
					    component.writeValue(null)
 | 
				
			||||||
    expect(component.value).toEqual([])
 | 
					    expect(component.value).toEqual([])
 | 
				
			||||||
    expect(component.selectedDocuments).toEqual([])
 | 
					    expect(component.selectedDocuments).toEqual([])
 | 
				
			||||||
@@ -100,9 +115,14 @@ describe('DocumentLinkComponent', () => {
 | 
				
			|||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  it('should support unselect', () => {
 | 
					  it('should support unselect', () => {
 | 
				
			||||||
    const getSpy = jest.spyOn(documentService, 'getCachedMany')
 | 
					    const getSpy = jest.spyOn(documentService, 'getFew')
 | 
				
			||||||
    getSpy.mockImplementation((ids) => {
 | 
					    getSpy.mockImplementation((ids) => {
 | 
				
			||||||
      return of(documents.filter((d) => ids.includes(d.id)))
 | 
					      const docs = documents.filter((d) => ids.includes(d.id))
 | 
				
			||||||
 | 
					      return of({
 | 
				
			||||||
 | 
					        count: docs.length,
 | 
				
			||||||
 | 
					        all: docs.map((d) => d.id),
 | 
				
			||||||
 | 
					        results: docs,
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    component.writeValue([12, 23])
 | 
					    component.writeValue([12, 23])
 | 
				
			||||||
    component.unselect({ id: 23 })
 | 
					    component.unselect({ id: 23 })
 | 
				
			||||||
@@ -115,4 +135,26 @@ describe('DocumentLinkComponent', () => {
 | 
				
			|||||||
    expect(component.compareDocuments(documents[0], { id: 2 })).toBeFalsy()
 | 
					    expect(component.compareDocuments(documents[0], { id: 2 })).toBeFalsy()
 | 
				
			||||||
    expect(component.trackByFn(documents[1])).toEqual(12)
 | 
					    expect(component.trackByFn(documents[1])).toEqual(12)
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  it('should not include the current document or already selected documents in results', () => {
 | 
				
			||||||
 | 
					    let foundDocs
 | 
				
			||||||
 | 
					    component.foundDocuments$.subscribe((found) => (foundDocs = found))
 | 
				
			||||||
 | 
					    component.parentDocumentID = 23
 | 
				
			||||||
 | 
					    component.selectedDocuments = [documents[2]]
 | 
				
			||||||
 | 
					    const listSpy = jest.spyOn(documentService, 'listFiltered')
 | 
				
			||||||
 | 
					    listSpy.mockImplementation(
 | 
				
			||||||
 | 
					      (page, pageSize, sortField, sortReverse, filterRules, extraParams) => {
 | 
				
			||||||
 | 
					        const docs = documents.filter((d) =>
 | 
				
			||||||
 | 
					          d.title.includes(filterRules[0].value)
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        return of({
 | 
				
			||||||
 | 
					          count: docs.length,
 | 
				
			||||||
 | 
					          results: docs,
 | 
				
			||||||
 | 
					          all: docs.map((d) => d.id),
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					    component.documentsInput$.next('bar')
 | 
				
			||||||
 | 
					    expect(foundDocs).toEqual([documents[1]])
 | 
				
			||||||
 | 
					  })
 | 
				
			||||||
})
 | 
					})
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -43,6 +43,9 @@ export class DocumentLinkComponent
 | 
				
			|||||||
  @Input()
 | 
					  @Input()
 | 
				
			||||||
  notFoundText: string = $localize`No documents found`
 | 
					  notFoundText: string = $localize`No documents found`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @Input()
 | 
				
			||||||
 | 
					  parentDocumentID: number
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(private documentsService: DocumentService) {
 | 
					  constructor(private documentsService: DocumentService) {
 | 
				
			||||||
    super()
 | 
					    super()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -58,11 +61,11 @@ export class DocumentLinkComponent
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this.loading = true
 | 
					      this.loading = true
 | 
				
			||||||
      this.documentsService
 | 
					      this.documentsService
 | 
				
			||||||
        .getCachedMany(documentIDs)
 | 
					        .getFew(documentIDs, { fields: 'id,title' })
 | 
				
			||||||
        .pipe(takeUntil(this.unsubscribeNotifier))
 | 
					        .pipe(takeUntil(this.unsubscribeNotifier))
 | 
				
			||||||
        .subscribe((documents) => {
 | 
					        .subscribe((documentResults) => {
 | 
				
			||||||
          this.loading = false
 | 
					          this.loading = false
 | 
				
			||||||
          this.selectedDocuments = documents
 | 
					          this.selectedDocuments = documentResults.results
 | 
				
			||||||
          super.writeValue(documentIDs)
 | 
					          super.writeValue(documentIDs)
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -86,7 +89,13 @@ export class DocumentLinkComponent
 | 
				
			|||||||
              { truncate_content: true }
 | 
					              { truncate_content: true }
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            .pipe(
 | 
					            .pipe(
 | 
				
			||||||
              map((results) => results.results),
 | 
					              map((results) =>
 | 
				
			||||||
 | 
					                results.results.filter(
 | 
				
			||||||
 | 
					                  (d) =>
 | 
				
			||||||
 | 
					                    d.id !== this.parentDocumentID &&
 | 
				
			||||||
 | 
					                    !this.selectedDocuments.find((sd) => sd.id === d.id)
 | 
				
			||||||
 | 
					                )
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
              catchError(() => of([])), // empty on error
 | 
					              catchError(() => of([])), // empty on error
 | 
				
			||||||
              tap(() => (this.loading = false))
 | 
					              tap(() => (this.loading = false))
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -124,7 +124,7 @@
 | 
				
			|||||||
                                    <pngx-input-number *ngSwitchCase="PaperlessCustomFieldDataType.Monetary" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".01" [error]="getCustomFieldError(i)"></pngx-input-number>
 | 
					                                    <pngx-input-number *ngSwitchCase="PaperlessCustomFieldDataType.Monetary" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [showAdd]="false" [step]=".01" [error]="getCustomFieldError(i)"></pngx-input-number>
 | 
				
			||||||
                                    <pngx-input-check *ngSwitchCase="PaperlessCustomFieldDataType.Boolean" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true"></pngx-input-check>
 | 
					                                    <pngx-input-check *ngSwitchCase="PaperlessCustomFieldDataType.Boolean" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true"></pngx-input-check>
 | 
				
			||||||
                                    <pngx-input-url *ngSwitchCase="PaperlessCustomFieldDataType.Url" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-url>
 | 
					                                    <pngx-input-url *ngSwitchCase="PaperlessCustomFieldDataType.Url" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-url>
 | 
				
			||||||
                                    <pngx-input-document-link *ngSwitchCase="PaperlessCustomFieldDataType.DocumentLink" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-document-link>
 | 
					                                    <pngx-input-document-link *ngSwitchCase="PaperlessCustomFieldDataType.DocumentLink" formControlName="value" [title]="getCustomFieldFromInstance(fieldInstance)?.name" [parentDocumentID]="documentId" [removable]="true" (removed)="removeField(fieldInstance)" [horizontal]="true" [error]="getCustomFieldError(i)"></pngx-input-document-link>
 | 
				
			||||||
                                </div>
 | 
					                                </div>
 | 
				
			||||||
                            </ng-container>
 | 
					                            </ng-container>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,6 +96,21 @@ export const commonAbstractPaperlessServiceTests = (endpoint, ServiceClass) => {
 | 
				
			|||||||
      expect(req.request.method).toEqual('PATCH')
 | 
					      expect(req.request.method).toEqual('PATCH')
 | 
				
			||||||
      req.flush([])
 | 
					      req.flush([])
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    test('should call appropriate api endpoint for get a few objects', () => {
 | 
				
			||||||
 | 
					      subscription = service.getFew([1, 2, 3]).subscribe()
 | 
				
			||||||
 | 
					      const req = httpTestingController.expectOne(
 | 
				
			||||||
 | 
					        `${environment.apiBaseUrl}${endpoint}/?id__in=1,2,3`
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      expect(req.request.method).toEqual('GET')
 | 
				
			||||||
 | 
					      req.flush([])
 | 
				
			||||||
 | 
					      subscription = service.getFew([4, 5, 6], { foo: 'bar' }).subscribe()
 | 
				
			||||||
 | 
					      const req2 = httpTestingController.expectOne(
 | 
				
			||||||
 | 
					        `${environment.apiBaseUrl}${endpoint}/?id__in=4,5,6&foo=bar`
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      expect(req2.request.method).toEqual('GET')
 | 
				
			||||||
 | 
					      req2.flush([])
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  beforeEach(() => {
 | 
					  beforeEach(() => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -91,6 +91,19 @@ export abstract class AbstractPaperlessService<T extends ObjectWithId> {
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  getFew(ids: number[], extraParams?): Observable<Results<T>> {
 | 
				
			||||||
 | 
					    let httpParams = new HttpParams()
 | 
				
			||||||
 | 
					    httpParams = httpParams.set('id__in', ids.join(','))
 | 
				
			||||||
 | 
					    for (let extraParamKey in extraParams) {
 | 
				
			||||||
 | 
					      if (extraParams[extraParamKey] != null) {
 | 
				
			||||||
 | 
					        httpParams = httpParams.set(extraParamKey, extraParams[extraParamKey])
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return this.http.get<Results<T>>(this.getResourceUrl(), {
 | 
				
			||||||
 | 
					      params: httpParams,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  clearCache() {
 | 
					  clearCache() {
 | 
				
			||||||
    this._listAll = null
 | 
					    this._listAll = null
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user