Frontend display custom fields in dasboard views

This commit is contained in:
shamoon 2024-04-17 16:00:30 -07:00
parent cbea10fb24
commit ac94173690
7 changed files with 267 additions and 49 deletions

View File

@ -111,7 +111,7 @@
</h6> </h6>
} }
<ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)"> <ul class="nav flex-column mb-2" cdkDropList (cdkDropListDropped)="onDrop($event)">
@for (view of savedViewService.sidebarViews; track view) { @for (view of savedViewService.sidebarViews; track view.id) {
<li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews" <li class="nav-item w-100 app-link" cdkDrag [cdkDragDisabled]="!settingsService.organizingSidebarSavedViews"
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)" cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
(cdkDragEnded)="onDragEnd($event)"> (cdkDragEnded)="onDragEnd($event)">

View File

@ -23,7 +23,7 @@
} }
<ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }"> <ng-container *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.SavedView }">
@for (v of dashboardViews; track v) { @for (v of dashboardViews; track v.id) {
<div class="col"> <div class="col">
<pngx-saved-view-widget <pngx-saved-view-widget
[savedView]="v" [savedView]="v"

View File

@ -14,7 +14,7 @@
<thead> <thead>
<tr> <tr>
@for (column of savedView.dashboard_view_table_columns; track column; let i = $index) { @for (column of savedView.dashboard_view_table_columns; track column; let i = $index) {
@if (columnIsVisible(column)) { @if (visibleColumns.includes(column)) {
<th <th
scope="col" scope="col"
[ngClass]="{ [ngClass]="{
@ -28,10 +28,10 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (doc of documents; track doc) { @for (doc of documents; track doc.id) {
<tr (mouseleave)="maybeClosePopover()"> <tr>
@for (column of savedView.dashboard_view_table_columns; track column; let i = $index) { @for (column of savedView.dashboard_view_table_columns; track column; let i = $index) {
@if (columnIsVisible(column)) { @if (visibleColumns.includes(column)) {
<td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': i > 1 }"> <td class="py-2 py-md-3 position-relative" [ngClass]="{ 'd-none d-md-table-cell': i > 1 }">
@switch (column) { @switch (column) {
@case (DashboardViewTableColumn.ADDED) { @case (DashboardViewTableColumn.ADDED) {
@ -64,6 +64,31 @@
} }
} }
} }
@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) }}
}
}
}
@if (i === savedView.dashboard_view_table_columns.length - 1) { @if (i === savedView.dashboard_view_table_columns.length - 1) {
<div class="btn-group position-absolute top-50 end-0 translate-middle-y"> <div class="btn-group position-absolute top-50 end-0 translate-middle-y">
<a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle" <a [href]="getPreviewUrl(doc)" title="View Preview" i18n-title target="_blank" class="btn px-4 btn-dark border-dark-subtle"

View File

@ -40,6 +40,8 @@ import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
import { DragDropModule } from '@angular/cdk/drag-drop' import { DragDropModule } from '@angular/cdk/drag-drop'
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component' import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons' 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'
const savedView: SavedView = { const savedView: SavedView = {
id: 1, id: 1,
@ -61,6 +63,10 @@ const savedView: SavedView = {
DashboardViewTableColumn.TITLE, DashboardViewTableColumn.TITLE,
DashboardViewTableColumn.TAGS, DashboardViewTableColumn.TAGS,
DashboardViewTableColumn.CORRESPONDENT, DashboardViewTableColumn.CORRESPONDENT,
DashboardViewTableColumn.DOCUMENT_TYPE,
DashboardViewTableColumn.STORAGE_PATH,
`${DashboardViewTableColumn.CUSTOM_FIELD}11` as any,
`${DashboardViewTableColumn.CUSTOM_FIELD}15` as any,
], ],
} }
@ -68,11 +74,35 @@ const documentResults = [
{ {
id: 2, id: 2,
title: 'doc2', title: 'doc2',
custom_fields: [
{ id: 1, field: 11, created: new Date(), value: 'custom', document: 2 },
],
}, },
{ {
id: 3, id: 3,
title: 'doc3', title: 'doc3',
correspondent: 0, correspondent: 0,
custom_fields: [],
},
{
id: 4,
title: 'doc4',
custom_fields: [
{ id: 32, field: 3, created: new Date(), value: 'EUR123', document: 4 },
],
},
{
id: 5,
title: 'doc5',
custom_fields: [
{
id: 22,
field: 15,
created: new Date(),
value: [123, 456, 789],
document: 5,
},
],
}, },
] ]
@ -106,6 +136,33 @@ describe('SavedViewWidgetComponent', () => {
}, },
CustomDatePipe, CustomDatePipe,
DatePipe, DatePipe,
{
provide: CustomFieldsService,
useValue: {
listAll: () =>
of({
all: [3, 11, 15],
count: 3,
results: [
{
id: 3,
name: 'Custom field 3',
data_type: CustomFieldDataType.Monetary,
},
{
id: 11,
name: 'Custom Field 11',
data_type: CustomFieldDataType.String,
},
{
id: 15,
name: 'Custom Field 15',
data_type: CustomFieldDataType.DocumentLink,
},
],
}),
},
},
], ],
imports: [ imports: [
HttpClientTestingModule, HttpClientTestingModule,
@ -289,52 +346,97 @@ describe('SavedViewWidgetComponent', () => {
it('should check if column is visible including permissions', () => { it('should check if column is visible including permissions', () => {
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.TITLE) component.visibleColumns.includes(DashboardViewTableColumn.TITLE)
).toBeTruthy() ).toBeTruthy()
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.CREATED) component.visibleColumns.includes(DashboardViewTableColumn.CREATED)
).toBeTruthy() ).toBeTruthy()
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.ADDED) component.visibleColumns.includes(DashboardViewTableColumn.ADDED)
).toBeTruthy() ).toBeTruthy()
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.TAGS) component.visibleColumns.includes(DashboardViewTableColumn.TAGS)
).toBeTruthy() ).toBeTruthy()
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.CORRESPONDENT) component.visibleColumns.includes(DashboardViewTableColumn.CORRESPONDENT)
).toBeTruthy() ).toBeTruthy()
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.DOCUMENT_TYPE) component.visibleColumns.includes(DashboardViewTableColumn.DOCUMENT_TYPE)
).toBeTruthy() ).toBeTruthy()
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.STORAGE_PATH) component.visibleColumns.includes(DashboardViewTableColumn.STORAGE_PATH)
).toBeTruthy()
expect(
component.visibleColumns.includes(
`${DashboardViewTableColumn.CUSTOM_FIELD}11` as any
)
).toBeTruthy() ).toBeTruthy()
component.visibleColumns = []
jest jest
.spyOn(component.permissionsService, 'currentUserCan') .spyOn(component.permissionsService, 'currentUserCan')
.mockReturnValue(false) .mockReturnValue(false)
component.ngOnInit()
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.TITLE) component.visibleColumns.includes(DashboardViewTableColumn.TAGS)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CREATED)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.ADDED)
).toBeTruthy()
expect(component.columnIsVisible(DashboardViewTableColumn.TAGS)).toBeFalsy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CORRESPONDENT)
).toBeFalsy() ).toBeFalsy()
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.DOCUMENT_TYPE) component.visibleColumns.includes(DashboardViewTableColumn.CORRESPONDENT)
).toBeFalsy() ).toBeFalsy()
expect( expect(
component.columnIsVisible(DashboardViewTableColumn.STORAGE_PATH) component.visibleColumns.includes(DashboardViewTableColumn.DOCUMENT_TYPE)
).toBeFalsy() ).toBeFalsy()
expect(
component.visibleColumns.includes(DashboardViewTableColumn.STORAGE_PATH)
).toBeFalsy()
expect(
component.visibleColumns.includes(
`${DashboardViewTableColumn.CUSTOM_FIELD}11` as any
)
).toBeFalsy()
})
it('should display monetary custom field value', () => {
expect( expect(
component.columnIsVisible('unknown' as DashboardViewTableColumn) component.getMonetaryCustomFieldValue(
).toBeFalsy() // coverage documentResults[2],
`${DashboardViewTableColumn.CUSTOM_FIELD}3`
)
).toEqual([123, 'EUR'])
expect(
component.getMonetaryCustomFieldValue(
documentResults[0],
`${DashboardViewTableColumn.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

@ -32,6 +32,9 @@ import {
PermissionType, PermissionType,
PermissionsService, PermissionsService,
} from 'src/app/services/permissions.service' } from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { Results } from 'src/app/data/results'
@Component({ @Component({
selector: 'pngx-saved-view-widget', selector: 'pngx-saved-view-widget',
@ -44,9 +47,12 @@ export class SavedViewWidgetComponent
{ {
public DashboardViewMode = DashboardViewMode public DashboardViewMode = DashboardViewMode
public DashboardViewTableColumn = DashboardViewTableColumn public DashboardViewTableColumn = DashboardViewTableColumn
public CustomFieldDataType = CustomFieldDataType
loading: boolean = true loading: boolean = true
private customFields: CustomField[] = []
constructor( constructor(
private documentService: DocumentService, private documentService: DocumentService,
private router: Router, private router: Router,
@ -54,7 +60,8 @@ export class SavedViewWidgetComponent
private consumerStatusService: ConsumerStatusService, private consumerStatusService: ConsumerStatusService,
public openDocumentsService: OpenDocumentsService, public openDocumentsService: OpenDocumentsService,
public documentListViewService: DocumentListViewService, public documentListViewService: DocumentListViewService,
public permissionsService: PermissionsService public permissionsService: PermissionsService,
private customFieldService: CustomFieldsService
) { ) {
super() super()
} }
@ -72,6 +79,14 @@ export class SavedViewWidgetComponent
mouseOnPreview = false mouseOnPreview = false
popoverHidden = true popoverHidden = true
visibleColumns: DashboardViewTableColumn[] = [
DashboardViewTableColumn.TITLE,
DashboardViewTableColumn.CREATED,
DashboardViewTableColumn.ADDED,
]
docLinkDocuments: Document[] = []
ngOnInit(): void { ngOnInit(): void {
this.reload() this.reload()
this.consumerStatusService this.consumerStatusService
@ -80,6 +95,35 @@ export class SavedViewWidgetComponent
.subscribe(() => { .subscribe(() => {
this.reload() this.reload()
}) })
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.CustomField
)
) {
this.customFieldService
.listAll()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((customFields) => {
this.customFields = customFields.results
this.maybeGetDocuments()
})
}
this.savedView.dashboard_view_table_columns?.forEach((column) => {
let type: PermissionType = Object.values(PermissionType).find((t) =>
t.includes(column)
)
if (column.startsWith(DashboardViewTableColumn.CUSTOM_FIELD)) {
type = PermissionType.CustomField
}
if (
type &&
this.permissionsService.currentUserCan(PermissionAction.View, type)
)
this.visibleColumns.push(column)
})
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -102,6 +146,7 @@ export class SavedViewWidgetComponent
.subscribe((result) => { .subscribe((result) => {
this.loading = false this.loading = false
this.documents = result.results this.documents = result.results
this.maybeGetDocuments()
}) })
} }
@ -204,26 +249,73 @@ export class SavedViewWidgetComponent
}, 300) }, 300)
} }
public columnIsVisible(column: DashboardViewTableColumn): boolean { public getColumnTitle(column: DashboardViewTableColumn): string {
if ( if (column.startsWith(DashboardViewTableColumn.CUSTOM_FIELD)) {
[ const id = column.split('_')[2]
DashboardViewTableColumn.TITLE, return this.customFields.find((c) => c.id === parseInt(id))?.name
DashboardViewTableColumn.CREATED, }
DashboardViewTableColumn.ADDED, return DASHBOARD_VIEW_TABLE_COLUMNS.find((c) => c.id === column)?.name
].includes(column) }
) {
return true public getCustomFieldDataType(column_id: string): string {
} else { const customFieldId = parseInt(column_id.split('_')[2])
const type: PermissionType = Object.values(PermissionType).find((t) => return this.customFields.find((cf) => cf.id === customFieldId)?.data_type
t.includes(column) }
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.dashboard_view_table_columns
?.filter((column) =>
column.startsWith(DashboardViewTableColumn.CUSTOM_FIELD)
) )
return type .forEach((column) => {
? this.permissionsService.currentUserCan(PermissionAction.View, type) if (
: false 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 getColumnTitle(column: DashboardViewTableColumn): string { public getDocumentTitle(documentId: number): string {
return DASHBOARD_VIEW_TABLE_COLUMNS.find((c) => c.id === column)?.name return this.docLinkDocuments.find((doc) => doc.id === documentId)?.title
} }
} }

View File

@ -14,6 +14,7 @@ export enum DashboardViewTableColumn {
CORRESPONDENT = 'correspondent', CORRESPONDENT = 'correspondent',
DOCUMENT_TYPE = 'documenttype', DOCUMENT_TYPE = 'documenttype',
STORAGE_PATH = 'storagepath', STORAGE_PATH = 'storagepath',
CUSTOM_FIELD = 'custom_field_',
} }
export const DASHBOARD_VIEW_TABLE_COLUMNS = [ export const DASHBOARD_VIEW_TABLE_COLUMNS = [

View File

@ -1,9 +1,7 @@
import { Injectable } from '@angular/core' import { Injectable } from '@angular/core'
import { HttpClient, HttpParams } from '@angular/common/http' import { HttpClient } from '@angular/common/http'
import { AbstractPaperlessService } from './abstract-paperless-service' import { AbstractPaperlessService } from './abstract-paperless-service'
import { Observable } from 'rxjs'
import { CustomField } from 'src/app/data/custom-field' import { CustomField } from 'src/app/data/custom-field'
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',