Feature: Implement custom fields for documents (#4502)

Adds custom fields of certain data types, attachable to documents and searchable

Co-Authored-By: Trenton H <797416+stumpylog@users.noreply.github.com>
This commit is contained in:
shamoon
2023-11-05 17:26:51 -08:00
parent 800f54f263
commit 10729f0362
67 changed files with 3199 additions and 421 deletions

View File

@@ -67,6 +67,9 @@ import { PageHeaderComponent } from '../common/page-header/page-header.component
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
import { DocumentDetailComponent } from './document-detail.component'
import { ShareLinksDropdownComponent } from '../common/share-links-dropdown/share-links-dropdown.component'
import { CustomFieldsDropdownComponent } from '../common/custom-fields-dropdown/custom-fields-dropdown.component'
import { PaperlessCustomFieldDataType } from 'src/app/data/paperless-custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
const doc: PaperlessDocument = {
id: 3,
@@ -94,8 +97,31 @@ const doc: PaperlessDocument = {
user: 2,
},
],
custom_fields: [
{
field: 0,
document: 3,
created: new Date(),
value: 'custom foo bar',
},
],
}
const customFields = [
{
id: 0,
name: 'Field 1',
data_type: PaperlessCustomFieldDataType.String,
created: new Date(),
},
{
id: 1,
name: 'Custom Field 2',
data_type: PaperlessCustomFieldDataType.Integer,
created: new Date(),
},
]
describe('DocumentDetailComponent', () => {
let component: DocumentDetailComponent
let fixture: ComponentFixture<DocumentDetailComponent>
@@ -107,6 +133,7 @@ describe('DocumentDetailComponent', () => {
let toastService: ToastService
let documentListViewService: DocumentListViewService
let settingsService: SettingsService
let customFieldsService: CustomFieldsService
let currentUserCan = true
let currentUserHasObjectPermissions = true
@@ -136,6 +163,7 @@ describe('DocumentDetailComponent', () => {
PdfViewerComponent,
SafeUrlPipe,
ShareLinksDropdownComponent,
CustomFieldsDropdownComponent,
],
providers: [
DocumentTitlePipe,
@@ -199,6 +227,7 @@ describe('DocumentDetailComponent', () => {
}),
},
},
CustomFieldsService,
{
provide: PermissionsService,
useValue: {
@@ -234,6 +263,7 @@ describe('DocumentDetailComponent', () => {
toastService = TestBed.inject(ToastService)
documentListViewService = TestBed.inject(DocumentListViewService)
settingsService = TestBed.inject(SettingsService)
customFieldsService = TestBed.inject(CustomFieldsService)
fixture = TestBed.createComponent(DocumentDetailComponent)
component = fixture.componentInstance
})
@@ -290,6 +320,13 @@ describe('DocumentDetailComponent', () => {
it('should load already-opened document via param', () => {
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(doc)
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({
count: customFields.length,
all: customFields.map((f) => f.id),
results: customFields,
})
)
fixture.detectChanges() // calls ngOnInit
expect(component.document).toEqual(doc)
})
@@ -797,12 +834,92 @@ describe('DocumentDetailComponent', () => {
expect(toastSpy).toHaveBeenCalledWith('Error retrieving metadata', error)
})
it('should display custom fields', () => {
initNormally()
expect(fixture.debugElement.nativeElement.textContent).toContain(
customFields[0].name
)
})
it('should support add custom field, correctly send via post', () => {
initNormally()
const initialLength = doc.custom_fields.length
expect(component.customFieldFormFields).toHaveLength(initialLength)
component.addField(customFields[1])
fixture.detectChanges()
expect(component.document.custom_fields).toHaveLength(initialLength + 1)
expect(component.customFieldFormFields).toHaveLength(initialLength + 1)
expect(fixture.debugElement.nativeElement.textContent).toContain(
customFields[1].name
)
const updateSpy = jest.spyOn(documentService, 'update')
component.save(true)
expect(updateSpy.mock.lastCall[0].custom_fields).toHaveLength(2)
expect(updateSpy.mock.lastCall[0].custom_fields[1]).toEqual({
field: customFields[1].id,
value: null,
})
})
it('should support remove custom field, correctly send via post', () => {
initNormally()
const initialLength = doc.custom_fields.length
expect(component.customFieldFormFields).toHaveLength(initialLength)
component.removeField(doc.custom_fields[0])
fixture.detectChanges()
expect(component.document.custom_fields).toHaveLength(initialLength - 1)
expect(component.customFieldFormFields).toHaveLength(initialLength - 1)
expect(fixture.debugElement.nativeElement.textContent).not.toContain(
'Field 1'
)
const updateSpy = jest.spyOn(documentService, 'update')
component.save(true)
expect(updateSpy.mock.lastCall[0].custom_fields).toHaveLength(
initialLength - 1
)
})
it('should show custom field errors', () => {
initNormally()
component.error = {
custom_fields: [
{},
{},
{ value: ['This field may not be null.'] },
{},
{ non_field_errors: ['Enter a valid URL.'] },
],
}
expect(component.getCustomFieldError(2)).toEqual([
'This field may not be null.',
])
expect(component.getCustomFieldError(4)).toEqual(['Enter a valid URL.'])
})
it('should refresh custom fields when created', () => {
initNormally()
const refreshSpy = jest.spyOn(component, 'refreshCustomFields')
fixture.debugElement
.query(By.directive(CustomFieldsDropdownComponent))
.triggerEventHandler('created')
expect(refreshSpy).toHaveBeenCalled()
})
function initNormally() {
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
jest
.spyOn(documentService, 'get')
.mockReturnValueOnce(of(Object.assign({}, doc)))
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(null)
jest
.spyOn(openDocumentsService, 'openDocument')
.mockReturnValueOnce(of(true))
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({
count: customFields.length,
all: customFields.map((f) => f.id),
results: customFields,
})
)
fixture.detectChanges()
}
})