Real display of custom fields

This commit is contained in:
shamoon
2024-04-18 02:08:49 -07:00
parent 149b1538ac
commit d8e7113fef
22 changed files with 387 additions and 277 deletions

View File

@@ -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()
})

View File

@@ -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,

View File

@@ -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)
})
})

View File

@@ -0,0 +1,25 @@
@if (field) {
@switch (field.data_type) {
@case (CustomFieldDataType.Monetary) {
<span>{{value | currency: currency}}</span>
}
@case (CustomFieldDataType.Date) {
<span>{{value | customDate}}</span>
}
@case (CustomFieldDataType.Url) {
<a [href]="value" class="btn-link text-dark text-decoration-none" target="_blank">{{value}}</a>
}
@case (CustomFieldDataType.DocumentLink) {
<div class="d-flex gap-1 flex-wrap">
@for (docId of value; track docId) {
<a routerLink="/documents/{{docId}}" class="badge bg-dark text-primary" title="View" i18n-title>
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs>&nbsp;<span>{{ getDocumentTitle(docId) }}</span>
</a>
}
</div>
}
@default {
<span>{{value}}</span>
}
}
}

View File

@@ -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<CustomFieldDisplayComponent>
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')
})
})

View File

@@ -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<any> = 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<Document>) => {
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()
}
}

View File

@@ -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) {
<a [href]="getCustomFieldValue(doc, column)" class="btn-link text-dark text-decoration-none" target="_blank">{{ getCustomFieldValue(doc, column) }}</a>
}
@case (CustomFieldDataType.DocumentLink) {
<div class="d-flex gap-1 flex-wrap">
@for (docId of getCustomFieldValue(doc, column); track docId) {
<a routerLink="/documents/{{docId}}" class="badge bg-dark text-primary" title="View" i18n-title>
<i-bs width="0.9em" height="0.9em" name="file-text"></i-bs>&nbsp;<span>{{ getDocumentTitle(docId) }}</span>
</a>
}
</div>
}
@default {
{{ getCustomFieldValue(doc, column) }}
}
}
<pngx-custom-field-display [document]="doc" [fieldDisplayKey]="column"></pngx-custom-field-display>
}
@if (i === savedView.document_display_fields.length - 1) {
<div class="btn-group position-absolute top-50 end-0 translate-middle-y">

View File

@@ -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
})
})

View File

@@ -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<number | string> {
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<Document>) => {
this.docLinkDocuments = result.results
})
}
}
public getDocumentTitle(documentId: number): string {
return this.docLinkDocuments.find((doc) => doc.id === documentId)?.title
}
}

View File

@@ -123,6 +123,16 @@
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
</div>
}
@for (field of document.custom_fields; track field.id) {
@if (displayFields.has(DocumentDisplayField.CUSTOM_FIELD + field.field)) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="ui-radios"></i-bs>
<small>
<pngx-custom-field-display [document]="document" [fieldId]="field.field"></pngx-custom-field-display>
</small>
</div>
}
}
</div>
</div>
</div>

View File

@@ -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: [

View File

@@ -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<DocumentDisplayField>
displayFields: Set<DocumentDisplayField | string> = new Set(
DOCUMENT_DISPLAY_FIELDS.map((f) => f.id)
)
@Output()
toggleSelected = new EventEmitter()

View File

@@ -97,8 +97,8 @@
@for (field of document.custom_fields; track field.id) {
@if (displayFields.has(DocumentDisplayField.CUSTOM_FIELD + field.field)) {
<div class="ps-0 p-1">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="info-circle"></i-bs>
<small>{{field.value}}</small>
<i-bs width="1em" height="1em" class="me-2 text-muted" name="ui-radios"></i-bs>
<pngx-custom-field-display [document]="document" [fieldId]="field.field"></pngx-custom-field-display>
</div>
}
}

View File

@@ -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: [

View File

@@ -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<DocumentDisplayField | string>
displayFields: Set<DocumentDisplayField | string> = new Set(
DOCUMENT_DISPLAY_FIELDS.map((f) => f.id)
)
@Output()
dblClickDocument = new EventEmitter()

View File

@@ -182,7 +182,6 @@
<th
pngxSortable="title"
title="Sort by title" i18n-title
class="w-40"
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)"
@@ -258,12 +257,12 @@
</div>
</td>
@if (activeDisplayFields.has(DocumentDisplayField.ASN)) {
<td class="d-none d-lg-table-cell">
<td class="d-none d-xl-table-cell">
{{d.archive_serial_number}}
</td>
}
@if (activeDisplayFields.has(DocumentDisplayField.CORRESPONDENT) && permissionService.currentUserCan(PermissionAction.View, PermissionType.Correspondent)) {
<td class="d-none d-md-table-cell">
<td class="d-none d-xl-table-cell">
@if (d.correspondent) {
<a (click)="clickCorrespondent(d.correspondent);$event.stopPropagation()" title="Filter by correspondent" i18n-title>{{(d.correspondent$ | async)?.name}}</a>
}
@@ -320,8 +319,8 @@
</td>
}
@for (field of activeDisplayCustomFields; track field) {
<td>
{{getCustomFieldValue(d, field)}}
<td class="d-none d-xl-table-cell">
<pngx-custom-field-display [document]="d" [fieldDisplayKey]="field"></pngx-custom-field-display>
</td>
}
</tr>

View File

@@ -10,10 +10,6 @@ th {
cursor: pointer;
}
th.w-40 {
width: 40%;
}
.table-row-selected {
background-color: var(--pngx-primary-faded);
}

View File

@@ -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'
)
})
})

View File

@@ -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')

View File

@@ -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)
})
})

View File

@@ -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 ??