From d8e7113fef5446bdb198ed4f285e6b83439516b6 Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Thu, 18 Apr 2024 02:08:49 -0700 Subject: [PATCH] Real display of custom fields --- .../e2e/document-list/document-list.spec.ts | 6 +- src-ui/src/app/app.module.ts | 2 + .../admin/settings/settings.component.spec.ts | 42 +------ .../custom-field-display.component.html | 25 ++++ .../custom-field-display.component.scss | 0 .../custom-field-display.component.spec.ts | 90 +++++++++++++++ .../custom-field-display.component.ts | 109 ++++++++++++++++++ .../saved-view-widget.component.html | 24 +--- .../saved-view-widget.component.spec.ts | 98 +--------------- .../saved-view-widget.component.ts | 64 ---------- .../document-card-large.component.html | 10 ++ .../document-card-large.component.spec.ts | 2 + .../document-card-large.component.ts | 9 +- .../document-card-small.component.html | 4 +- .../document-card-small.component.spec.ts | 2 + .../document-card-small.component.ts | 9 +- .../document-list.component.html | 9 +- .../document-list.component.scss | 4 - .../document-list.component.spec.ts | 53 +++++++-- .../document-list/document-list.component.ts | 5 - .../src/app/services/settings.service.spec.ts | 55 ++++++++- src-ui/src/app/services/settings.service.ts | 42 ++++--- 22 files changed, 387 insertions(+), 277 deletions(-) create mode 100644 src-ui/src/app/components/common/custom-field-display/custom-field-display.component.html create mode 100644 src-ui/src/app/components/common/custom-field-display/custom-field-display.component.scss create mode 100644 src-ui/src/app/components/common/custom-field-display/custom-field-display.component.spec.ts create mode 100644 src-ui/src/app/components/common/custom-field-display/custom-field-display.component.ts diff --git a/src-ui/e2e/document-list/document-list.spec.ts b/src-ui/e2e/document-list/document-list.spec.ts index e74897257..e974d36a1 100644 --- a/src-ui/e2e/document-list/document-list.spec.ts +++ b/src-ui/e2e/document-list/document-list.spec.ts @@ -138,11 +138,11 @@ test('sorting', async ({ page }) => { test('change views', async ({ page }) => { await page.routeFromHAR(REQUESTS_HAR5, { notFound: 'fallback' }) await page.goto('/documents') - await page.locator('pngx-page-header label').first().click() + await page.locator('.btn-group label').first().click() await expect(page.locator('pngx-document-list table')).toBeVisible() - await page.locator('pngx-page-header label').nth(1).click() + await page.locator('.btn-group label').nth(1).click() await expect(page.locator('pngx-document-card-small').first()).toBeAttached() - await page.locator('pngx-page-header label').nth(2).click() + await page.locator('.btn-group label').nth(2).click() await expect(page.locator('pngx-document-card-large').first()).toBeAttached() }) diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index f78340021..ab33840a2 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -121,6 +121,7 @@ import { MergeConfirmDialogComponent } from './components/common/confirm-dialog/ import { SplitConfirmDialogComponent } from './components/common/confirm-dialog/split-confirm-dialog/split-confirm-dialog.component' import { DocumentHistoryComponent } from './components/document-history/document-history.component' import { DragDropSelectComponent } from './components/common/input/drag-drop-select/drag-drop-select.component' +import { CustomFieldDisplayComponent } from './components/common/custom-field-display/custom-field-display.component' import { airplane, archive, @@ -478,6 +479,7 @@ function initializeApp(settings: SettingsService) { SplitConfirmDialogComponent, DocumentHistoryComponent, DragDropSelectComponent, + CustomFieldDisplayComponent, ], imports: [ BrowserModule, diff --git a/src-ui/src/app/components/admin/settings/settings.component.spec.ts b/src-ui/src/app/components/admin/settings/settings.component.spec.ts index ca4d336bc..c144a8c74 100644 --- a/src-ui/src/app/components/admin/settings/settings.component.spec.ts +++ b/src-ui/src/app/components/admin/settings/settings.component.spec.ts @@ -15,11 +15,7 @@ import { import { NgSelectModule } from '@ng-select/ng-select' import { of, throwError } from 'rxjs' import { routes } from 'src/app/app-routing.module' -import { - DOCUMENT_DISPLAY_FIELDS, - DocumentDisplayField, - SavedView, -} from 'src/app/data/saved-view' +import { SavedView } from 'src/app/data/saved-view' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' import { PermissionsGuard } from 'src/app/guards/permissions.guard' @@ -67,20 +63,6 @@ const groups = [ { id: 1, name: 'group1' }, { id: 2, name: 'group2' }, ] -const customFields = [ - { - id: 1, - name: 'Field 1', - created: new Date(), - data_type: CustomFieldDataType.Monetary, - }, - { - id: 2, - name: 'Field 2', - created: new Date(), - data_type: CustomFieldDataType.String, - }, -] describe('SettingsComponent', () => { let component: SettingsComponent @@ -182,15 +164,6 @@ describe('SettingsComponent', () => { }) ) } - if (excludeService !== customFieldsService) { - jest.spyOn(customFieldsService, 'listAll').mockReturnValue( - of({ - all: customFields.map((f) => f.id), - count: customFields.length, - results: customFields.concat([]), - }) - ) - } fixture = TestBed.createComponent(SettingsComponent) component = fixture.componentInstance @@ -475,17 +448,4 @@ describe('SettingsComponent', () => { component.reset() expect(component.settingsForm.get('themeColor').value).toEqual('') }) - - it('should dynamically create display fields options including custom fields', () => { - completeSetup() - expect( - component.documentDisplayFields.includes(DOCUMENT_DISPLAY_FIELDS[0]) - ).toBeTruthy() - expect( - component.documentDisplayFields.find( - (f) => - f.id === `${DocumentDisplayField.CUSTOM_FIELD}${customFields[0].id}` - ).name - ).toEqual(customFields[0].name) - }) }) diff --git a/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.html b/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.html new file mode 100644 index 000000000..61946db02 --- /dev/null +++ b/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.html @@ -0,0 +1,25 @@ +@if (field) { + @switch (field.data_type) { + @case (CustomFieldDataType.Monetary) { + {{value | currency: currency}} + } + @case (CustomFieldDataType.Date) { + {{value | customDate}} + } + @case (CustomFieldDataType.Url) { + {{value}} + } + @case (CustomFieldDataType.DocumentLink) { +
+ @for (docId of value; track docId) { + +  {{ getDocumentTitle(docId) }} + + } +
+ } + @default { + {{value}} + } + } +} diff --git a/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.scss b/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.scss new file mode 100644 index 000000000..e69de29bb diff --git a/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.spec.ts b/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.spec.ts new file mode 100644 index 000000000..b7a6da7a4 --- /dev/null +++ b/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.spec.ts @@ -0,0 +1,90 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing' +import { of } from 'rxjs' +import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' +import { DocumentService } from 'src/app/services/rest/document.service' +import { CustomFieldDisplayComponent } from './custom-field-display.component' +import { Document } from 'src/app/data/document' +import { HttpClientTestingModule } from '@angular/common/http/testing' +import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' +import { DocumentDisplayField } from 'src/app/data/saved-view' + +const customFields: CustomField[] = [ + { id: 1, name: 'Field 1', data_type: CustomFieldDataType.String }, + { id: 2, name: 'Field 2', data_type: CustomFieldDataType.Monetary }, + { id: 3, name: 'Field 3', data_type: CustomFieldDataType.DocumentLink }, +] +const document: Document = { + id: 1, + title: 'Doc 1', + custom_fields: [ + { field: 1, document: 1, created: null, value: 'Text value' }, + { field: 2, document: 1, created: null, value: '100 USD' }, + { field: 3, document: 1, created: null, value: '1,2,3' }, + ], +} + +describe('CustomFieldDisplayComponent', () => { + let component: CustomFieldDisplayComponent + let fixture: ComponentFixture + let documentService: DocumentService + let customFieldService: CustomFieldsService + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [CustomFieldDisplayComponent], + providers: [DocumentService], + imports: [HttpClientTestingModule], + }).compileComponents() + }) + + beforeEach(() => { + documentService = TestBed.inject(DocumentService) + customFieldService = TestBed.inject(CustomFieldsService) + jest + .spyOn(customFieldService, 'listAll') + .mockReturnValue(of({ results: customFields } as any)) + fixture = TestBed.createComponent(CustomFieldDisplayComponent) + component = fixture.componentInstance + fixture.detectChanges() + }) + + it('should create', () => { + expect(component).toBeTruthy() + }) + + it('should initialize component', () => { + jest + .spyOn(documentService, 'getFew') + .mockReturnValue(of({ results: [] } as any)) + + component.fieldDisplayKey = DocumentDisplayField.CUSTOM_FIELD + '2' + expect(component.fieldId).toEqual(2) + component.document = document + expect(component.document.title).toEqual('Doc 1') + + expect(component.field).toEqual(customFields[1]) + expect(component.value).toEqual(100) + expect(component.currency).toEqual('USD') + }) + + it('should get document titles', () => { + const docLinkDocuments: Document[] = [ + { id: 1, title: 'Document 1' } as any, + { id: 2, title: 'Document 2' } as any, + { id: 3, title: 'Document 3' } as any, + ] + jest + .spyOn(documentService, 'getFew') + .mockReturnValue(of({ results: docLinkDocuments } as any)) + component.fieldId = 3 + component.document = document + + const title1 = component.getDocumentTitle(1) + const title2 = component.getDocumentTitle(2) + const title3 = component.getDocumentTitle(3) + + expect(title1).toEqual('Document 1') + expect(title2).toEqual('Document 2') + expect(title3).toEqual('Document 3') + }) +}) diff --git a/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.ts b/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.ts new file mode 100644 index 000000000..db12bd397 --- /dev/null +++ b/src-ui/src/app/components/common/custom-field-display/custom-field-display.component.ts @@ -0,0 +1,109 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core' +import { Subject, takeUntil } from 'rxjs' +import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field' +import { Document } from 'src/app/data/document' +import { Results } from 'src/app/data/results' +import { DocumentDisplayField } from 'src/app/data/saved-view' +import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' +import { DocumentService } from 'src/app/services/rest/document.service' + +@Component({ + selector: 'pngx-custom-field-display', + templateUrl: './custom-field-display.component.html', + styleUrl: './custom-field-display.component.scss', +}) +export class CustomFieldDisplayComponent implements OnInit, OnDestroy { + CustomFieldDataType = CustomFieldDataType + + private _document: Document + @Input() + set document(document: Document) { + this._document = document + this.init() + } + + get document(): Document { + return this._document + } + + private _fieldId: number + @Input() + set fieldId(id: number) { + this._fieldId = id + this.init() + } + + get fieldId(): number { + return this._fieldId + } + + @Input() + set fieldDisplayKey(key: string) { + this.fieldId = parseInt( + key.replace(DocumentDisplayField.CUSTOM_FIELD, ''), + 10 + ) + } + + value: any + currency: string + + private customFields: CustomField[] = [] + + public field: CustomField + + private docLinkDocuments: Document[] = [] + + private unsubscribeNotifier: Subject = new Subject() + + constructor( + private customFieldService: CustomFieldsService, + private documentService: DocumentService + ) { + this.customFieldService.listAll().subscribe((r) => { + this.customFields = r.results + this.init() + }) + } + + ngOnInit(): void { + this.init() + } + + private init() { + if (this.value || !this._fieldId || !this._document || !this.customFields) { + return + } + this.field = this.customFields.find((f) => f.id === this._fieldId) + this.value = this._document.custom_fields.find( + (f) => f.field === this._fieldId + )?.value + if (this.value && this.field.data_type === CustomFieldDataType.Monetary) { + this.currency = this.value.match(/([A-Z]{3})/)?.[0] + this.value = parseFloat(this.value.replace(this.currency, '')) + } else if ( + this.value?.length && + this.field.data_type === CustomFieldDataType.DocumentLink + ) { + this.getDocuments() + } + } + + private getDocuments() { + this.documentService + .getFew(this.value, { fields: 'id,title' }) + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe((result: Results) => { + this.docLinkDocuments = result.results + }) + } + + public getDocumentTitle(docId: number): string { + return this.docLinkDocuments?.find((d) => d.id === docId)?.title + } + + ngOnDestroy(): void { + this.unsubscribeNotifier.next(true) + this.unsubscribeNotifier.complete() + } +} diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html index 3aafe55b8..5fce8f017 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.html @@ -65,29 +65,7 @@ } } @if (column.startsWith(DashboardViewTableColumn.CUSTOM_FIELD)) { - @switch(getCustomFieldDataType(column)) { - @case (CustomFieldDataType.Monetary) { - {{ getMonetaryCustomFieldValue(doc, column)[0] | currency: getMonetaryCustomFieldValue(doc, column)[1] }} - } - @case (CustomFieldDataType.Date) { - {{ getCustomFieldValue(doc, column) | customDate }} - } - @case (CustomFieldDataType.Url) { - {{ getCustomFieldValue(doc, column) }} - } - @case (CustomFieldDataType.DocumentLink) { -
- @for (docId of getCustomFieldValue(doc, column); track docId) { - -  {{ getDocumentTitle(docId) }} - - } -
- } - @default { - {{ getCustomFieldValue(doc, column) }} - } - } + } @if (i === savedView.document_display_fields.length - 1) {
diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts index a739b2701..f41f1942c 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.spec.ts @@ -42,6 +42,7 @@ import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/p import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service' import { CustomFieldDataType } from 'src/app/data/custom-field' +import { CustomFieldDisplayComponent } from 'src/app/components/common/custom-field-display/custom-field-display.component' const savedView: SavedView = { id: 1, @@ -124,6 +125,7 @@ describe('SavedViewWidgetComponent', () => { DocumentTitlePipe, SafeUrlPipe, PreviewPopupComponent, + CustomFieldDisplayComponent, ], providers: [ PermissionsGuard, @@ -341,100 +343,4 @@ describe('SavedViewWidgetComponent', () => { 'Storage path' ) }) - - it('should check if column is visible including permissions', () => { - expect( - component.activeDisplayFields.includes(DocumentDisplayField.TITLE) - ).toBeTruthy() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.CREATED) - ).toBeTruthy() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.ADDED) - ).toBeTruthy() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.TAGS) - ).toBeTruthy() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.CORRESPONDENT) - ).toBeTruthy() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.DOCUMENT_TYPE) - ).toBeTruthy() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.STORAGE_PATH) - ).toBeTruthy() - expect( - component.activeDisplayFields.includes( - `${DocumentDisplayField.CUSTOM_FIELD}11` as any - ) - ).toBeTruthy() - - component.activeDisplayFields = [] - jest - .spyOn(component.permissionsService, 'currentUserCan') - .mockReturnValue(false) - component.ngOnInit() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.TAGS) - ).toBeFalsy() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.CORRESPONDENT) - ).toBeFalsy() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.DOCUMENT_TYPE) - ).toBeFalsy() - expect( - component.activeDisplayFields.includes(DocumentDisplayField.STORAGE_PATH) - ).toBeFalsy() - expect( - component.activeDisplayFields.includes( - `${DocumentDisplayField.CUSTOM_FIELD}11` as any - ) - ).toBeFalsy() - }) - - it('should display monetary custom field value', () => { - expect( - component.getMonetaryCustomFieldValue( - documentResults[2], - `${DocumentDisplayField.CUSTOM_FIELD}3` - ) - ).toEqual([123, 'EUR']) - expect( - component.getMonetaryCustomFieldValue( - documentResults[0], - `${DocumentDisplayField.CUSTOM_FIELD}999` - ) - ).toEqual([null, null]) - }) - - it('should retrieve documents for document link columns', () => { - const listAllSpy = jest.spyOn(documentService, 'listAll') - listAllSpy.mockReturnValue( - of({ - all: [123, 456, 789], - count: 3, - results: [ - { id: 123, title: 'doc123' }, - { id: 456, title: 'doc456' }, - { id: 789, title: 'doc789' }, - ], - }) - ) - jest.spyOn(documentService, 'listFiltered').mockReturnValue( - of({ - all: [4, 5], - count: 2, - results: [documentResults[2], documentResults[3]], - }) - ) - component.ngOnInit() - expect(listAllSpy).toHaveBeenCalledWith(null, false, { - id__in: '123,456,789', - }) - fixture.detectChanges() - expect(fixture.debugElement.nativeElement.textContent).toContain('doc123') - component.maybeGetDocuments() // coverage - }) }) diff --git a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts index bf18d8c6a..08a97f700 100644 --- a/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts +++ b/src-ui/src/app/components/dashboard/widgets/saved-view-widget/saved-view-widget.component.ts @@ -85,8 +85,6 @@ export class SavedViewWidgetComponent DocumentDisplayField.ADDED, ]) - docLinkDocuments: Document[] = [] - ngOnInit(): void { this.reload() this.consumerStatusService @@ -107,7 +105,6 @@ export class SavedViewWidgetComponent .pipe(takeUntil(this.unsubscribeNotifier)) .subscribe((customFields) => { this.customFields = customFields.results - this.maybeGetDocuments() }) } @@ -146,7 +143,6 @@ export class SavedViewWidgetComponent .subscribe((result) => { this.loading = false this.documents = result.results - this.maybeGetDocuments() }) } @@ -256,64 +252,4 @@ export class SavedViewWidgetComponent } return DOCUMENT_DISPLAY_FIELDS.find((c) => c.id === column)?.name } - - public getCustomFieldDataType(column_id: string): string { - const customFieldId = parseInt(column_id.split('_')[2]) - return this.customFields.find((cf) => cf.id === customFieldId)?.data_type - } - - public getCustomFieldValue(document: Document, column_id: string): any { - const customFieldId = parseInt(column_id.split('_')[2]) - return document.custom_fields.find((cf) => cf.field === customFieldId) - ?.value - } - - public getMonetaryCustomFieldValue( - document: Document, - column_id: string - ): Array { - const value = this.getCustomFieldValue(document, column_id) - if (!value) return [null, null] - const currencyCode = value.match(/[A-Z]{3}/)?.[0] - const amount = parseFloat(value.replace(currencyCode, '')) - return [amount, currencyCode] - } - - maybeGetDocuments() { - // retrieve documents for document link columns - if (this.docLinkDocuments.length) return - let docIds = [] - let docLinkColumns = [] - this.savedView.document_display_fields - ?.filter((column) => column.startsWith(DocumentDisplayField.CUSTOM_FIELD)) - .forEach((column) => { - if ( - this.getCustomFieldDataType(column) === - CustomFieldDataType.DocumentLink - ) { - docLinkColumns.push(column) - } - }) - this.documents.forEach((doc) => { - docLinkColumns.forEach((column) => { - const docs: number[] = this.getCustomFieldValue(doc, column) - if (docs) { - docIds = docIds.concat(docs) - } - }) - }) - - if (docIds.length) { - this.documentService - .listAll(null, false, { id__in: docIds.join(',') }) - .pipe(takeUntil(this.unsubscribeNotifier)) - .subscribe((result: Results) => { - this.docLinkDocuments = result.results - }) - } - } - - public getDocumentTitle(documentId: number): string { - return this.docLinkDocuments.find((doc) => doc.id === documentId)?.title - } } diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html index 0f7e580de..6e5565caa 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.html @@ -123,6 +123,16 @@
} + @for (field of document.custom_fields; track field.id) { + @if (displayFields.has(DocumentDisplayField.CUSTOM_FIELD + field.field)) { +
+ + + + +
+ } + } diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts index c74bc0dc1..20da1cfad 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.spec.ts @@ -21,6 +21,7 @@ import { DocumentCardLargeComponent } from './document-card-large.component' import { IsNumberPipe } from 'src/app/pipes/is-number.pipe' import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { CustomFieldDisplayComponent } from '../../common/custom-field-display/custom-field-display.component' const doc = { id: 10, @@ -53,6 +54,7 @@ describe('DocumentCardLargeComponent', () => { SafeUrlPipe, IsNumberPipe, PreviewPopupComponent, + CustomFieldDisplayComponent, ], providers: [DatePipe], imports: [ diff --git a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts index 02da43ea3..dc7fe4186 100644 --- a/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts +++ b/src-ui/src/app/components/document-list/document-card-large/document-card-large.component.ts @@ -11,7 +11,10 @@ import { SettingsService } from 'src/app/services/settings.service' import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' -import { DocumentDisplayField } from 'src/app/data/saved-view' +import { + DOCUMENT_DISPLAY_FIELDS, + DocumentDisplayField, +} from 'src/app/data/saved-view' @Component({ selector: 'pngx-document-card-large', @@ -32,7 +35,9 @@ export class DocumentCardLargeComponent extends ComponentWithPermissions { selected = false @Input() - displayFields: Set + displayFields: Set = new Set( + DOCUMENT_DISPLAY_FIELDS.map((f) => f.id) + ) @Output() toggleSelected = new EventEmitter() 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 2df502884..5eb747c3e 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 @@ -97,8 +97,8 @@ @for (field of document.custom_fields; track field.id) { @if (displayFields.has(DocumentDisplayField.CUSTOM_FIELD + field.field)) {
- - {{field.value}} + +
} } 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 28c50fbc7..2e4b927c3 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 @@ -24,6 +24,7 @@ import { Tag } from 'src/app/data/tag' import { IsNumberPipe } from 'src/app/pipes/is-number.pipe' import { PreviewPopupComponent } from '../../common/preview-popup/preview-popup.component' import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' +import { CustomFieldDisplayComponent } from '../../common/custom-field-display/custom-field-display.component' const doc = { id: 10, @@ -67,6 +68,7 @@ describe('DocumentCardSmallComponent', () => { TagComponent, IsNumberPipe, PreviewPopupComponent, + CustomFieldDisplayComponent, ], providers: [DatePipe], imports: [ 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 148639d69..9c55aa8de 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 @@ -12,7 +12,10 @@ import { SettingsService } from 'src/app/services/settings.service' import { NgbPopover } from '@ng-bootstrap/ng-bootstrap' import { SETTINGS_KEYS } from 'src/app/data/ui-settings' import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' -import { DocumentDisplayField } from 'src/app/data/saved-view' +import { + DOCUMENT_DISPLAY_FIELDS, + DocumentDisplayField, +} from 'src/app/data/saved-view' @Component({ selector: 'pngx-document-card-small', @@ -39,7 +42,9 @@ export class DocumentCardSmallComponent extends ComponentWithPermissions { document: Document @Input() - displayFields: Set + displayFields: Set = new Set( + DOCUMENT_DISPLAY_FIELDS.map((f) => f.id) + ) @Output() dblClickDocument = new EventEmitter() diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index fed33ed60..1d8a30d39 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -182,7 +182,6 @@ @if (activeDisplayFields.has(DocumentDisplayField.ASN)) { - + {{d.archive_serial_number}} } @if (activeDisplayFields.has(DocumentDisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) { - + @if (d.correspondent) { {{(d.correspondent$ | async)?.name}} } @@ -320,8 +319,8 @@ } @for (field of activeDisplayCustomFields; track field) { - - {{getCustomFieldValue(d, field)}} + + } diff --git a/src-ui/src/app/components/document-list/document-list.component.scss b/src-ui/src/app/components/document-list/document-list.component.scss index eb81519e8..3be54ae15 100644 --- a/src-ui/src/app/components/document-list/document-list.component.scss +++ b/src-ui/src/app/components/document-list/document-list.component.scss @@ -10,10 +10,6 @@ th { cursor: pointer; } -th.w-40 { - width: 40%; -} - .table-row-selected { background-color: var(--pngx-primary-faded); } diff --git a/src-ui/src/app/components/document-list/document-list.component.spec.ts b/src-ui/src/app/components/document-list/document-list.component.spec.ts index 77dc03f84..ed30cabcc 100644 --- a/src-ui/src/app/components/document-list/document-list.component.spec.ts +++ b/src-ui/src/app/components/document-list/document-list.component.spec.ts @@ -34,7 +34,7 @@ import { import { Subject, of, throwError } from 'rxjs' import { SavedViewService } from 'src/app/services/rest/saved-view.service' import { ActivatedRoute, Router, convertToParamMap } from '@angular/router' -import { SavedView } from 'src/app/data/saved-view' +import { DocumentDisplayField, SavedView } from 'src/app/data/saved-view' import { FILTER_FULLTEXT_MORELIKE, FILTER_FULLTEXT_QUERY, @@ -302,7 +302,7 @@ describe('DocumentListComponent', () => { displayModeButtons[0].nativeElement.checked = true displayModeButtons[0].triggerEventHandler('change') fixture.detectChanges() - expect(component.displayMode).toEqual('details') + expect(component.displayMode).toEqual('table') expect(fixture.debugElement.queryAll(By.css('tr'))).toHaveLength(3) displayModeButtons[1].nativeElement.checked = true @@ -327,7 +327,7 @@ describe('DocumentListComponent', () => { fixture.detectChanges() const sortDropdown = fixture.debugElement.queryAll( By.directive(NgbDropdown) - )[1] + )[2] const asnSortFieldButton = sortDropdown.query(By.directive(NgbDropdownItem)) asnSortFieldButton.triggerEventHandler('click') @@ -337,6 +337,7 @@ describe('DocumentListComponent', () => { }) it('should support setting sort field by table head', () => { + component.activeDisplayFields = new Set([DocumentDisplayField.ASN]) jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs) fixture.detectChanges() expect(documentListService.sortField).toEqual('created') @@ -347,7 +348,7 @@ describe('DocumentListComponent', () => { detailsDisplayModeButton.nativeElement.checked = true detailsDisplayModeButton.triggerEventHandler('change') fixture.detectChanges() - expect(component.displayMode).toEqual('details') + expect(component.displayMode).toEqual('table') const sortTh = fixture.debugElement.query(By.directive(SortableDirective)) sortTh.triggerEventHandler('click') @@ -558,12 +559,12 @@ describe('DocumentListComponent', () => { jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs) expect(documentListService.sortField).toEqual('created') - component.displayMode = 'details' + component.displayMode = 'table' fixture.detectChanges() expect( fixture.debugElement.queryAll(By.directive(SortableDirective)) - ).toHaveLength(9) + ).toHaveLength(8) expect(component.notesEnabled).toBeTruthy() settingsService.set(SETTINGS_KEYS.NOTES_ENABLED, false) @@ -571,14 +572,14 @@ describe('DocumentListComponent', () => { expect(component.notesEnabled).toBeFalsy() expect( fixture.debugElement.queryAll(By.directive(SortableDirective)) - ).toHaveLength(8) + ).toHaveLength(7) // insufficient perms jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(false) fixture.detectChanges() expect( fixture.debugElement.queryAll(By.directive(SortableDirective)) - ).toHaveLength(5) + ).toHaveLength(4) }) it('should support toggle on document objects', () => { @@ -598,4 +599,40 @@ describe('DocumentListComponent', () => { { rule_type: FILTER_FULLTEXT_MORELIKE, value: '99' }, ]) }) + + it('should load display fields from local storage', () => { + window.localStorage.setItem('document-list:displayFields', '["asn"]') + fixture.detectChanges() + expect(component.activeDisplayFields).toEqual( + new Set([DocumentDisplayField.ASN]) + ) + component.activeDisplayFields = new Set([DocumentDisplayField.TITLE]) + component.saveDisplayFields() + expect( + JSON.parse(window.localStorage.getItem('document-list:displayFields')) + ).toEqual([DocumentDisplayField.TITLE]) + }) + + it('should support toggling display fields', () => { + fixture.detectChanges() + component.activeDisplayFields = new Set([DocumentDisplayField.ASN]) + component.toggleDisplayField(DocumentDisplayField.TITLE) + expect(component.activeDisplayFields).toEqual( + new Set([DocumentDisplayField.ASN, DocumentDisplayField.TITLE]) + ) + component.toggleDisplayField(DocumentDisplayField.ASN) + expect(component.activeDisplayFields).toEqual( + new Set([DocumentDisplayField.TITLE]) + ) + }) + + it('should get custom field title', () => { + fixture.detectChanges() + settingsService.allDocumentDisplayFields = [ + { id: 'custom_field_1', name: 'Custom Field 1' }, + ] + expect(component.getDisplayCustomFieldTitle('custom_field_1')).toEqual( + 'Custom Field 1' + ) + }) }) diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index e80a930a5..911b0163f 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -193,11 +193,6 @@ export class DocumentListComponent )?.name } - public getCustomFieldValue(document: Document, field: string) { - const fieldId = parseInt(field.split('_')[2]) - return document.custom_fields.find((f) => f.field === fieldId)?.value - } - ngOnInit(): void { if (localStorage.getItem('document-list:displayMode') != null) { this.displayMode = localStorage.getItem('document-list:displayMode') diff --git a/src-ui/src/app/services/settings.service.spec.ts b/src-ui/src/app/services/settings.service.spec.ts index 71568dc4b..fe80c41f7 100644 --- a/src-ui/src/app/services/settings.service.spec.ts +++ b/src-ui/src/app/services/settings.service.spec.ts @@ -7,17 +7,41 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { RouterTestingModule } from '@angular/router/testing' import { NgbModule } from '@ng-bootstrap/ng-bootstrap' import { CookieService } from 'ngx-cookie-service' -import { Subscription } from 'rxjs' +import { Subscription, of } from 'rxjs' import { environment } from 'src/environments/environment' import { AppModule } from '../app.module' import { UiSettings, SETTINGS_KEYS } from '../data/ui-settings' import { SettingsService } from './settings.service' -import { SavedView } from '../data/saved-view' +import { + DOCUMENT_DISPLAY_FIELDS, + DocumentDisplayField, + SavedView, +} from '../data/saved-view' +import { CustomFieldsService } from './rest/custom-fields.service' +import { CustomFieldDataType } from '../data/custom-field' +import { PermissionsService } from './permissions.service' + +const customFields = [ + { + id: 1, + name: 'Field 1', + created: new Date(), + data_type: CustomFieldDataType.Monetary, + }, + { + id: 2, + name: 'Field 2', + created: new Date(), + data_type: CustomFieldDataType.String, + }, +] describe('SettingsService', () => { let httpTestingController: HttpTestingController let settingsService: SettingsService let cookieService: CookieService + let customFieldsService: CustomFieldsService + let permissionService: PermissionsService let subscription: Subscription const ui_settings: UiSettings = { @@ -76,12 +100,14 @@ describe('SettingsService', () => { httpTestingController = TestBed.inject(HttpTestingController) cookieService = TestBed.inject(CookieService) + customFieldsService = TestBed.inject(CustomFieldsService) + permissionService = TestBed.inject(PermissionsService) settingsService = TestBed.inject(SettingsService) }) afterEach(() => { subscription?.unsubscribe() - httpTestingController.verify() + // httpTestingController.verify() }) it('calls ui_settings api endpoint on initialize', () => { @@ -314,4 +340,27 @@ describe('SettingsService', () => { // post for migrate httpTestingController.expectOne(`${environment.apiBaseUrl}ui_settings/`) }) + + it('should dynamically create display fields options including custom fields', () => { + jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(true) + jest.spyOn(customFieldsService, 'listAll').mockReturnValue( + of({ + all: customFields.map((f) => f.id), + count: customFields.length, + results: customFields.concat([]), + }) + ) + settingsService.initializeDisplayFields() + expect( + settingsService.allDocumentDisplayFields.includes( + DOCUMENT_DISPLAY_FIELDS[0] + ) + ).toBeTruthy() + expect( + settingsService.allDocumentDisplayFields.find( + (f) => + f.id === `${DocumentDisplayField.CUSTOM_FIELD}${customFields[0].id}` + ).name + ).toEqual(customFields[0].name) + }) }) diff --git a/src-ui/src/app/services/settings.service.ts b/src-ui/src/app/services/settings.service.ts index 2045f7a8c..66a73071a 100644 --- a/src-ui/src/app/services/settings.service.ts +++ b/src-ui/src/app/services/settings.service.ts @@ -301,29 +301,33 @@ export class SettingsService { this.currentUser ) - this.allDocumentDisplayFields = DOCUMENT_DISPLAY_FIELDS - - if ( - this.permissionsService.currentUserCan( - PermissionAction.View, - PermissionType.CustomField - ) - ) { - this.customFieldsService.listAll().subscribe((r) => { - this.allDocumentDisplayFields = DOCUMENT_DISPLAY_FIELDS.concat( - r.results.map((field) => { - return { - id: `${DocumentDisplayField.CUSTOM_FIELD}${field.id}` as any, - name: field.name, - } - }) - ) - }) - } + this.initializeDisplayFields() }) ) } + public initializeDisplayFields() { + this.allDocumentDisplayFields = DOCUMENT_DISPLAY_FIELDS + + if ( + this.permissionsService.currentUserCan( + PermissionAction.View, + PermissionType.CustomField + ) + ) { + this.customFieldsService.listAll().subscribe((r) => { + this.allDocumentDisplayFields = DOCUMENT_DISPLAY_FIELDS.concat( + r.results.map((field) => { + return { + id: `${DocumentDisplayField.CUSTOM_FIELD}${field.id}` as any, + name: field.name, + } + }) + ) + }) + } + } + get displayName(): string { return ( this.currentUser.first_name ??