Frontend display custom fields in dasboard views

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

View File

@ -111,7 +111,7 @@
</h6>
}
<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"
cdkDragPreviewContainer="parent" cdkDragPreviewClass="navItemDrag" (cdkDragStarted)="onDragStart($event)"
(cdkDragEnded)="onDragEnd($event)">

View File

@ -23,7 +23,7 @@
}
<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">
<pngx-saved-view-widget
[savedView]="v"

View File

@ -14,7 +14,7 @@
<thead>
<tr>
@for (column of savedView.dashboard_view_table_columns; track column; let i = $index) {
@if (columnIsVisible(column)) {
@if (visibleColumns.includes(column)) {
<th
scope="col"
[ngClass]="{
@ -28,10 +28,10 @@
</tr>
</thead>
<tbody>
@for (doc of documents; track doc) {
<tr (mouseleave)="maybeClosePopover()">
@for (doc of documents; track doc.id) {
<tr>
@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 }">
@switch (column) {
@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) {
<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"

View File

@ -40,6 +40,8 @@ import { SafeUrlPipe } from 'src/app/pipes/safeurl.pipe'
import { DragDropModule } from '@angular/cdk/drag-drop'
import { PreviewPopupComponent } from 'src/app/components/common/preview-popup/preview-popup.component'
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 = {
id: 1,
@ -61,6 +63,10 @@ const savedView: SavedView = {
DashboardViewTableColumn.TITLE,
DashboardViewTableColumn.TAGS,
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,
title: 'doc2',
custom_fields: [
{ id: 1, field: 11, created: new Date(), value: 'custom', document: 2 },
],
},
{
id: 3,
title: 'doc3',
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,
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: [
HttpClientTestingModule,
@ -289,52 +346,97 @@ describe('SavedViewWidgetComponent', () => {
it('should check if column is visible including permissions', () => {
expect(
component.columnIsVisible(DashboardViewTableColumn.TITLE)
component.visibleColumns.includes(DashboardViewTableColumn.TITLE)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CREATED)
component.visibleColumns.includes(DashboardViewTableColumn.CREATED)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.ADDED)
component.visibleColumns.includes(DashboardViewTableColumn.ADDED)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.TAGS)
component.visibleColumns.includes(DashboardViewTableColumn.TAGS)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CORRESPONDENT)
component.visibleColumns.includes(DashboardViewTableColumn.CORRESPONDENT)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.DOCUMENT_TYPE)
component.visibleColumns.includes(DashboardViewTableColumn.DOCUMENT_TYPE)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.STORAGE_PATH)
component.visibleColumns.includes(DashboardViewTableColumn.STORAGE_PATH)
).toBeTruthy()
expect(
component.visibleColumns.includes(
`${DashboardViewTableColumn.CUSTOM_FIELD}11` as any
)
).toBeTruthy()
component.visibleColumns = []
jest
.spyOn(component.permissionsService, 'currentUserCan')
.mockReturnValue(false)
component.ngOnInit()
expect(
component.columnIsVisible(DashboardViewTableColumn.TITLE)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CREATED)
).toBeTruthy()
expect(
component.columnIsVisible(DashboardViewTableColumn.ADDED)
).toBeTruthy()
expect(component.columnIsVisible(DashboardViewTableColumn.TAGS)).toBeFalsy()
expect(
component.columnIsVisible(DashboardViewTableColumn.CORRESPONDENT)
component.visibleColumns.includes(DashboardViewTableColumn.TAGS)
).toBeFalsy()
expect(
component.columnIsVisible(DashboardViewTableColumn.DOCUMENT_TYPE)
component.visibleColumns.includes(DashboardViewTableColumn.CORRESPONDENT)
).toBeFalsy()
expect(
component.columnIsVisible(DashboardViewTableColumn.STORAGE_PATH)
component.visibleColumns.includes(DashboardViewTableColumn.DOCUMENT_TYPE)
).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(
component.columnIsVisible('unknown' as DashboardViewTableColumn)
).toBeFalsy() // coverage
component.getMonetaryCustomFieldValue(
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,
PermissionsService,
} 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({
selector: 'pngx-saved-view-widget',
@ -44,9 +47,12 @@ export class SavedViewWidgetComponent
{
public DashboardViewMode = DashboardViewMode
public DashboardViewTableColumn = DashboardViewTableColumn
public CustomFieldDataType = CustomFieldDataType
loading: boolean = true
private customFields: CustomField[] = []
constructor(
private documentService: DocumentService,
private router: Router,
@ -54,7 +60,8 @@ export class SavedViewWidgetComponent
private consumerStatusService: ConsumerStatusService,
public openDocumentsService: OpenDocumentsService,
public documentListViewService: DocumentListViewService,
public permissionsService: PermissionsService
public permissionsService: PermissionsService,
private customFieldService: CustomFieldsService
) {
super()
}
@ -72,6 +79,14 @@ export class SavedViewWidgetComponent
mouseOnPreview = false
popoverHidden = true
visibleColumns: DashboardViewTableColumn[] = [
DashboardViewTableColumn.TITLE,
DashboardViewTableColumn.CREATED,
DashboardViewTableColumn.ADDED,
]
docLinkDocuments: Document[] = []
ngOnInit(): void {
this.reload()
this.consumerStatusService
@ -80,6 +95,35 @@ export class SavedViewWidgetComponent
.subscribe(() => {
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 {
@ -102,6 +146,7 @@ export class SavedViewWidgetComponent
.subscribe((result) => {
this.loading = false
this.documents = result.results
this.maybeGetDocuments()
})
}
@ -204,26 +249,73 @@ export class SavedViewWidgetComponent
}, 300)
}
public columnIsVisible(column: DashboardViewTableColumn): boolean {
if (
[
DashboardViewTableColumn.TITLE,
DashboardViewTableColumn.CREATED,
DashboardViewTableColumn.ADDED,
].includes(column)
) {
return true
} else {
const type: PermissionType = Object.values(PermissionType).find((t) =>
t.includes(column)
public getColumnTitle(column: DashboardViewTableColumn): string {
if (column.startsWith(DashboardViewTableColumn.CUSTOM_FIELD)) {
const id = column.split('_')[2]
return this.customFields.find((c) => c.id === parseInt(id))?.name
}
return DASHBOARD_VIEW_TABLE_COLUMNS.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.dashboard_view_table_columns
?.filter((column) =>
column.startsWith(DashboardViewTableColumn.CUSTOM_FIELD)
)
return type
? this.permissionsService.currentUserCan(PermissionAction.View, type)
: false
.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 getColumnTitle(column: DashboardViewTableColumn): string {
return DASHBOARD_VIEW_TABLE_COLUMNS.find((c) => c.id === column)?.name
public getDocumentTitle(documentId: number): string {
return this.docLinkDocuments.find((doc) => doc.id === documentId)?.title
}
}

View File

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

View File

@ -1,9 +1,7 @@
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 { Observable } from 'rxjs'
import { CustomField } from 'src/app/data/custom-field'
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
@Injectable({
providedIn: 'root',