Enhancement: custom field sorting (#8494)

This commit is contained in:
shamoon
2024-12-30 10:18:34 -08:00
committed by GitHub
parent e44cfef662
commit 4e3d25c714
10 changed files with 522 additions and 25 deletions

View File

@@ -5942,7 +5942,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">289</context>
<context context-type="linenumber">294</context>
</context-group>
</trans-unit>
<trans-unit id="78870852467682010" datatype="html">
@@ -5957,7 +5957,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">329</context>
<context context-type="linenumber">334</context>
</context-group>
</trans-unit>
<trans-unit id="157572966557284263" datatype="html">
@@ -5972,7 +5972,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">336</context>
<context context-type="linenumber">341</context>
</context-group>
</trans-unit>
<trans-unit id="872092479747931526" datatype="html">
@@ -7209,7 +7209,7 @@
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">305</context>
<context context-type="linenumber">310</context>
</context-group>
</trans-unit>
<trans-unit id="106713086593101376" datatype="html">
@@ -7573,25 +7573,32 @@
<context context-type="linenumber">261,263</context>
</context-group>
</trans-unit>
<trans-unit id="5083658411133224968" datatype="html">
<source>Sort by <x id="INTERPOLATION" equiv-text="{{getDisplayCustomFieldTitle(field_id)}}"/></source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">268,269</context>
</context-group>
</trans-unit>
<trans-unit id="2179847500064178686" datatype="html">
<source>Edit document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">297</context>
<context context-type="linenumber">302</context>
</context-group>
</trans-unit>
<trans-unit id="3420321797707163677" datatype="html">
<source>Preview document</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">298</context>
<context context-type="linenumber">303</context>
</context-group>
</trans-unit>
<trans-unit id="2807800733729323332" datatype="html">
<source>Yes</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">357</context>
<context context-type="linenumber">362</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>
@@ -7602,7 +7609,7 @@
<source>No</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/document-list/document-list.component.html</context>
<context context-type="linenumber">357</context>
<context context-type="linenumber">362</context>
</context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/pipes/yes-no.pipe.ts</context>

View File

@@ -262,9 +262,14 @@
Shared
</th>
}
@for (field of activeDisplayCustomFields; track field) {
<th>
{{getDisplayCustomFieldTitle(field)}}
@for (field_id of activeDisplayCustomFields; track field_id) {
<th class="cursor-pointer"
pngxSortable="{{field_id}}"
title="Sort by {{getDisplayCustomFieldTitle(field_id)}}" i18n-title
[currentSortField]="list.sortField"
[currentSortReverse]="list.sortReverse"
(sort)="onSort($event)">
{{getDisplayCustomFieldTitle(field_id)}}
</th>
}
</tr>

View File

@@ -11,6 +11,7 @@ import { SavedView } from 'src/app/data/saved-view'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsGuard } from 'src/app/guards/permissions.guard'
import { PermissionsService } from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { SavedViewService } from 'src/app/services/rest/saved-view.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmButtonComponent } from '../../common/confirm-button/confirm-button.component'
@@ -60,6 +61,17 @@ describe('SavedViewsComponent', () => {
currentUserCan: () => true,
},
},
{
provide: CustomFieldsService,
useValue: {
listAll: () =>
of({
all: [],
count: 0,
results: [],
}),
},
},
PermissionsGuard,
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),

View File

@@ -156,7 +156,7 @@ describe('DocumentListViewService', () => {
expect(documentListViewService.currentPage).toEqual(1)
})
it('should handle error on filtering request', () => {
it('should handle object error on filtering request', () => {
documentListViewService.currentPage = 1
const tags__id__in = 'hello'
const filterRulesAny = [
@@ -185,6 +185,50 @@ describe('DocumentListViewService', () => {
)
})
it('should handle object error on filtering request for custom field sorts', () => {
documentListViewService.currentPage = 1
documentListViewService.sortField = 'custom_field_999'
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-custom_field_999&truncate_content=true`
)
expect(req.request.method).toEqual('GET')
req.flush(
{ custom_field_999: ['Custom field not found'] },
{ status: 400, statusText: 'Unexpected error' }
)
expect(documentListViewService.error).toEqual(
'custom_field_999: Custom field not found'
)
// reset the list
documentListViewService.sortField = 'created'
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
)
})
it('should handle string error on filtering request', () => {
documentListViewService.currentPage = 1
const tags__id__in = 'hello'
const filterRulesAny = [
{
rule_type: FILTER_HAS_TAGS_ANY,
value: tags__id__in,
},
]
documentListViewService.filterRules = filterRulesAny
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true&tags__id__in=${tags__id__in}`
)
expect(req.request.method).toEqual('GET')
req.flush('Generic error', { status: 404, statusText: 'Unexpected error' })
expect(documentListViewService.error).toEqual('Generic error')
// reset the list
documentListViewService.filterRules = []
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
)
})
it('should support setting sort', () => {
expect(documentListViewService.sortField).toEqual('created')
expect(documentListViewService.sortReverse).toBeTruthy()

View File

@@ -307,18 +307,23 @@ export class DocumentListViewService {
activeListViewState.currentPage = 1
this.reload()
} else {
console.log(error)
this.selectionData = null
let errorMessage
if (
typeof error.error !== 'string' &&
typeof error.error === 'object' &&
Object.keys(error.error).length > 0
) {
// e.g. { archive_serial_number: Array<string> }
errorMessage = Object.keys(error.error)
.map((fieldName) => {
const fieldNameBase = fieldName.split('__')[0]
const fieldError: Array<string> = error.error[fieldName]
return `${
this.sortFields.find((f) => f.field == fieldName)?.name
this.sortFields.find(
(f) => f.field?.split('__')[0] == fieldNameBase
)?.name ?? fieldNameBase
}: ${fieldError[0]}`
})
.join(', ')

View File

@@ -4,7 +4,8 @@ import {
provideHttpClientTesting,
} from '@angular/common/http/testing'
import { TestBed } from '@angular/core/testing'
import { Subscription } from 'rxjs'
import { of, Subscription } from 'rxjs'
import { CustomFieldDataType } from 'src/app/data/custom-field'
import {
DOCUMENT_SORT_FIELDS,
DOCUMENT_SORT_FIELDS_FULLTEXT,
@@ -14,12 +15,15 @@ import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
import { environment } from 'src/environments/environment'
import { PermissionsService } from '../permissions.service'
import { SettingsService } from '../settings.service'
import { CustomFieldsService } from './custom-fields.service'
import { DocumentService } from './document.service'
let httpTestingController: HttpTestingController
let service: DocumentService
let subscription: Subscription
let settingsService: SettingsService
let permissionsService: PermissionsService
let customFieldsService: CustomFieldsService
const endpoint = 'documents'
const documents = [
@@ -55,8 +59,29 @@ beforeEach(() => {
})
httpTestingController = TestBed.inject(HttpTestingController)
service = TestBed.inject(DocumentService)
settingsService = TestBed.inject(SettingsService)
customFieldsService = TestBed.inject(CustomFieldsService)
permissionsService = TestBed.inject(PermissionsService)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({
all: [1, 2, 3],
count: 3,
results: [
{
id: 1,
name: 'Custom Field 1',
data_type: CustomFieldDataType.String,
},
{
id: 2,
name: 'Custom Field 2',
data_type: CustomFieldDataType.Integer,
},
],
})
)
service = TestBed.inject(DocumentService)
})
describe(`DocumentService`, () => {
@@ -289,18 +314,25 @@ describe(`DocumentService`, () => {
it('should construct sort fields respecting permissions', () => {
expect(
service.sortFields.find((f) => f.field === 'correspondent__name')
).toBeUndefined()
).not.toBeUndefined()
expect(
service.sortFields.find((f) => f.field === 'document_type__name')
).toBeUndefined()
).not.toBeUndefined()
expect(
service.sortFields.find((f) => f.field === 'owner')
).not.toBeUndefined()
const permissionsService: PermissionsService =
TestBed.inject(PermissionsService)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(false)
service['setupSortFields']()
expect(service.sortFields).toEqual(DOCUMENT_SORT_FIELDS)
const fields = DOCUMENT_SORT_FIELDS.filter(
(f) =>
['correspondent__name', 'document_type__name', 'owner'].indexOf(
f.field
) === -1
)
expect(service.sortFields).toEqual(fields)
expect(service.sortFieldsFullText).toEqual([
...DOCUMENT_SORT_FIELDS,
...fields,
...DOCUMENT_SORT_FIELDS_FULLTEXT,
])
@@ -311,6 +343,38 @@ it('should construct sort fields respecting permissions', () => {
).toBeUndefined()
})
it('should include custom fields in sort fields if user has permission', () => {
const permissionsService: PermissionsService =
TestBed.inject(PermissionsService)
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
service['customFields'] = [
{
id: 1,
name: 'Custom Field 1',
data_type: CustomFieldDataType.String,
},
{
id: 2,
name: 'Custom Field 2',
data_type: CustomFieldDataType.Integer,
},
]
service['setupSortFields']()
expect(service.sortFields).toEqual([
...DOCUMENT_SORT_FIELDS,
{
field: 'custom_field_1',
name: 'Custom Field 1',
},
{
field: 'custom_field_2',
name: 'Custom Field 2',
},
])
})
afterEach(() => {
subscription?.unsubscribe()
httpTestingController.verify()

View File

@@ -3,6 +3,7 @@ import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { map, tap } from 'rxjs/operators'
import { AuditLogEntry } from 'src/app/data/auditlog-entry'
import { CustomField } from 'src/app/data/custom-field'
import {
DOCUMENT_SORT_FIELDS,
DOCUMENT_SORT_FIELDS_FULLTEXT,
@@ -22,6 +23,7 @@ import {
import { SettingsService } from '../settings.service'
import { AbstractPaperlessService } from './abstract-paperless-service'
import { CorrespondentService } from './correspondent.service'
import { CustomFieldsService } from './custom-fields.service'
import { DocumentTypeService } from './document-type.service'
import { StoragePathService } from './storage-path.service'
import { TagService } from './tag.service'
@@ -55,6 +57,8 @@ export class DocumentService extends AbstractPaperlessService<Document> {
return this._sortFieldsFullText
}
private customFields: CustomField[] = []
constructor(
http: HttpClient,
private correspondentService: CorrespondentService,
@@ -62,14 +66,40 @@ export class DocumentService extends AbstractPaperlessService<Document> {
private tagService: TagService,
private storagePathService: StoragePathService,
private permissionsService: PermissionsService,
private settingsService: SettingsService
private settingsService: SettingsService,
private customFieldService: CustomFieldsService
) {
super(http, 'documents')
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.CustomField
)
) {
this.customFieldService.listAll().subscribe((fields) => {
this.customFields = fields.results
this.setupSortFields()
})
}
this.setupSortFields()
}
private setupSortFields() {
this._sortFields = [...DOCUMENT_SORT_FIELDS]
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
PermissionType.CustomField
)
) {
this.customFields.forEach((field) => {
this._sortFields.push({
field: `custom_field_${field.id}`,
name: field.name,
})
})
}
let excludes = []
if (
!this.permissionsService.currentUserCan(