Custom fields admin area, comment out detached fields code

This commit is contained in:
shamoon 2023-10-28 22:21:21 -07:00
parent 6f62da077a
commit 548ffd46b0
15 changed files with 538 additions and 46 deletions

View File

@ -24,6 +24,7 @@ import {
import { ConsumptionTemplatesComponent } from './components/manage/consumption-templates/consumption-templates.component' import { ConsumptionTemplatesComponent } from './components/manage/consumption-templates/consumption-templates.component'
import { MailComponent } from './components/manage/mail/mail.component' import { MailComponent } from './components/manage/mail/mail.component'
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component' import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
export const routes: Routes = [ export const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' }, { path: '', redirectTo: 'dashboard', pathMatch: 'full' },
@ -189,6 +190,17 @@ export const routes: Routes = [
}, },
}, },
}, },
{
path: 'customfields',
component: CustomFieldsComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.CustomField,
},
},
},
{ {
path: 'templates', path: 'templates',
component: ConsumptionTemplatesComponent, component: ConsumptionTemplatesComponent,

View File

@ -101,6 +101,8 @@ import { MailComponent } from './components/manage/mail/mail.component'
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component' import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
import { DragDropModule } from '@angular/cdk/drag-drop' import { DragDropModule } from '@angular/cdk/drag-drop'
import { FileDropComponent } from './components/file-drop/file-drop.component' import { FileDropComponent } from './components/file-drop/file-drop.component'
import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
import { CustomFieldEditDialogComponent } from './components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import localeAf from '@angular/common/locales/af' import localeAf from '@angular/common/locales/af'
import localeAr from '@angular/common/locales/ar' import localeAr from '@angular/common/locales/ar'
@ -246,6 +248,8 @@ function initializeApp(settings: SettingsService) {
MailComponent, MailComponent,
UsersAndGroupsComponent, UsersAndGroupsComponent,
FileDropComponent, FileDropComponent,
CustomFieldsComponent,
CustomFieldEditDialogComponent,
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

View File

@ -172,6 +172,13 @@
</svg><span>&nbsp;<ng-container i18n>Storage paths</ng-container></span> </svg><span>&nbsp;<ng-container i18n>Storage paths</ng-container></span>
</a> </a>
</li> </li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }">
<a class="nav-link" routerLink="customfields" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Custom fields" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#ui-radios"/>
</svg><span>&nbsp;<ng-container i18n>Custom Fields</ng-container></span>
</a>
</li>
<li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }" tourAnchor="tour.consumption-templates"> <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }" tourAnchor="tour.consumption-templates">
<a class="nav-link" routerLink="templates" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Consumption templates" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> <a class="nav-link" routerLink="templates" routerLinkActive="active" (click)="closeMenu()" ngbPopover="Consumption templates" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim">
<svg class="sidebaricon" fill="currentColor"> <svg class="sidebaricon" fill="currentColor">

View File

@ -0,0 +1,16 @@
<form [formGroup]="objectForm" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">{{getTitle()}}</h4>
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text>
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
<small class="d-block mt-n2" *ngIf="typeFieldDisabled" i18n>Data type cannot be changed after a field is created</small>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>
</form>

View File

@ -0,0 +1,67 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { CustomFieldEditDialogComponent } from './custom-field-edit-dialog.component'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { NgSelectModule } from '@ng-select/ng-select'
import { IfOwnerDirective } from 'src/app/directives/if-owner.directive'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { SafeHtmlPipe } from 'src/app/pipes/safehtml.pipe'
import { SettingsService } from 'src/app/services/settings.service'
import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component'
describe('CustomFieldEditDialogComponent', () => {
let component: CustomFieldEditDialogComponent
let settingsService: SettingsService
let fixture: ComponentFixture<CustomFieldEditDialogComponent>
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
CustomFieldEditDialogComponent,
IfPermissionsDirective,
IfOwnerDirective,
SelectComponent,
TextComponent,
SafeHtmlPipe,
],
providers: [NgbActiveModal],
imports: [
HttpClientTestingModule,
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
],
}).compileComponents()
fixture = TestBed.createComponent(CustomFieldEditDialogComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 99, username: 'user99' }
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create and edit modes', () => {
component.dialogMode = EditDialogMode.CREATE
const createTitleSpy = jest.spyOn(component, 'getCreateTitle')
const editTitleSpy = jest.spyOn(component, 'getEditTitle')
fixture.detectChanges()
expect(createTitleSpy).toHaveBeenCalled()
expect(editTitleSpy).not.toHaveBeenCalled()
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
expect(editTitleSpy).toHaveBeenCalled()
})
it('should disable data type select on edit', () => {
component.dialogMode = EditDialogMode.EDIT
fixture.detectChanges()
component.ngOnInit()
expect(component.objectForm.get('data_type').disabled).toBeTruthy()
})
})

View File

@ -0,0 +1,60 @@
import { Component, OnInit } from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import {
DATA_TYPE_LABELS,
PaperlessCustomField,
} from 'src/app/data/paperless-custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
@Component({
selector: 'pngx-custom-field-edit-dialog',
templateUrl: './custom-field-edit-dialog.component.html',
styleUrls: ['./custom-field-edit-dialog.component.scss'],
})
export class CustomFieldEditDialogComponent
extends EditDialogComponent<PaperlessCustomField>
implements OnInit
{
constructor(
service: CustomFieldsService,
activeModal: NgbActiveModal,
userService: UserService,
settingsService: SettingsService
) {
super(service, activeModal, userService, settingsService)
}
ngOnInit(): void {
super.ngOnInit()
if (this.typeFieldDisabled) {
this.objectForm.get('data_type').disable()
}
}
getCreateTitle() {
return $localize`Create new custom field`
}
getEditTitle() {
return $localize`Edit custom field`
}
getForm(): FormGroup {
return new FormGroup({
name: new FormControl(null),
data_type: new FormControl(null),
})
}
getDataTypes() {
return DATA_TYPE_LABELS
}
get typeFieldDisabled(): boolean {
return this.dialogMode === EditDialogMode.EDIT
}
}

View File

@ -393,28 +393,29 @@ export class DocumentDetailComponent
updateComponent(doc: PaperlessDocument) { updateComponent(doc: PaperlessDocument) {
this.document = doc this.document = doc
this.requiresPassword = false this.requiresPassword = false
this.customFieldsService // TODO: Custom fields
.getFields(doc.id) // this.customFieldsService
.pipe(first()) // .getFields(doc.id)
.subscribe({ // .pipe(first())
next: (fields) => { // .subscribe({
this.customFields = fields // next: (fields) => {
this.customFields.forEach((field) => { // this.customFields = fields
this.documentForm.addControl( // this.customFields.forEach((field) => {
`custom-field-${field.id}`, // this.documentForm.addControl(
new FormControl(field.data) // `custom-field-${field.id}`,
) // new FormControl(field.data)
}) // )
this.store.next(this.documentForm.value) // })
console.log(fields) // this.store.next(this.documentForm.value)
}, // console.log(fields)
error: (error) => { // },
this.toastService.showError( // error: (error) => {
$localize`Error retrieving custom fields`, // this.toastService.showError(
error // $localize`Error retrieving custom fields`,
) // error
}, // )
}) // },
// })
this.documentsService this.documentsService
.getMetadata(doc.id) .getMetadata(doc.id)
.pipe(first()) .pipe(first())
@ -541,11 +542,12 @@ export class DocumentDetailComponent
} }
this.title = doc.title this.title = doc.title
this.documentForm.patchValue(doc) this.documentForm.patchValue(doc)
this.customFields.forEach((field) => { // TODO: custom field reset
this.documentForm // this.customFields.forEach((field) => {
.get(`custom-field-${field.id}`) // this.documentForm
.patchValue(field.data) // .get(`custom-field-${field.id}`)
}) // .patchValue(field.data)
// })
this.openDocumentService.setDirty(doc, false) this.openDocumentService.setDirty(doc, false)
}, },
error: () => { error: () => {

View File

@ -0,0 +1,41 @@
<pngx-page-header title="Custom Fields">
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
<svg class="sidebaricon me-1" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#plus-circle" />
</svg>
<ng-container i18n>Add Field</ng-container>
</button>
</pngx-page-header>
<ul class="list-group">
<li class="list-group-item">
<div class="row">
<div class="col" i18n>Name</div>
<div class="col" i18n>Data Type</div>
<div class="col" i18n>Actions</div>
</div>
</li>
<li *ngFor="let field of fields" class="list-group-item">
<div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div>
<div class="col d-flex align-items-center">{{getDataType(field)}}</div>
<div class="col">
<div class="btn-group">
<button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#pencil" />
</svg>&nbsp;<ng-container i18n>Edit</ng-container>
</button>
<button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteField(field)">
<svg class="buttonicon-sm" fill="currentColor">
<use xlink:href="assets/bootstrap-icons.svg#trash" />
</svg>&nbsp;<ng-container i18n>Delete</ng-container>
</button>
</div>
</div>
</div>
</li>
<li *ngIf="fields.length === 0" class="list-group-item" i18n>No fields defined.</li>
</ul>

View File

@ -0,0 +1,162 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { CustomFieldsComponent } from './custom-fields.component'
import {
PaperlessCustomField,
PaperlessCustomFieldDataType,
} from 'src/app/data/paperless-custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { By } from '@angular/platform-browser'
import {
NgbModal,
NgbPaginationModule,
NgbModalModule,
NgbModalRef,
} from '@ng-bootstrap/ng-bootstrap'
import { of, throwError } from 'rxjs'
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
import { PermissionsService } from 'src/app/services/permissions.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { PageHeaderComponent } from '../../common/page-header/page-header.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
const fields: PaperlessCustomField[] = [
{
id: 0,
name: 'Field 1',
data_type: PaperlessCustomFieldDataType.String,
},
{
id: 1,
name: 'Field 2',
data_type: PaperlessCustomFieldDataType.Integer,
},
]
describe('CustomFieldsComponent', () => {
let component: CustomFieldsComponent
let fixture: ComponentFixture<CustomFieldsComponent>
let customFieldsService: CustomFieldsService
let modalService: NgbModal
let toastService: ToastService
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
CustomFieldsComponent,
IfPermissionsDirective,
PageHeaderComponent,
ConfirmDialogComponent,
],
providers: [
{
provide: PermissionsService,
useValue: {
currentUserCan: () => true,
currentUserHasObjectPermissions: () => true,
currentUserOwnsObject: () => true,
},
},
],
imports: [
HttpClientTestingModule,
NgbPaginationModule,
FormsModule,
ReactiveFormsModule,
NgbModalModule,
],
})
customFieldsService = TestBed.inject(CustomFieldsService)
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({
count: fields.length,
all: fields.map((o) => o.id),
results: fields,
})
)
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
fixture = TestBed.createComponent(CustomFieldsComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should support create, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const createButton = fixture.debugElement.queryAll(By.css('button'))[0]
createButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
// fail first
editDialog.failed.emit({ error: 'error creating item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(fields[0])
expect(toastInfoSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
})
it('should support edit, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
const reloadSpy = jest.spyOn(component, 'reload')
const editButton = fixture.debugElement.queryAll(By.css('button'))[1]
editButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
const editDialog = modal.componentInstance as CustomFieldEditDialogComponent
expect(editDialog.object).toEqual(fields[0])
// fail first
editDialog.failed.emit({ error: 'error editing item' })
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
editDialog.succeeded.emit(fields[0])
expect(toastInfoSpy).toHaveBeenCalled()
expect(reloadSpy).toHaveBeenCalled()
})
it('should support delete, show notification on error / success', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[m.length - 1]))
const toastErrorSpy = jest.spyOn(toastService, 'showError')
const deleteSpy = jest.spyOn(customFieldsService, 'delete')
const reloadSpy = jest.spyOn(component, 'reload')
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[3]
deleteButton.triggerEventHandler('click')
expect(modal).not.toBeUndefined()
const editDialog = modal.componentInstance as ConfirmDialogComponent
// fail first
deleteSpy.mockReturnValueOnce(throwError(() => new Error('error deleting')))
editDialog.confirmClicked.emit()
expect(toastErrorSpy).toHaveBeenCalled()
expect(reloadSpy).not.toHaveBeenCalled()
// succeed
deleteSpy.mockReturnValueOnce(of(true))
editDialog.confirmClicked.emit()
expect(reloadSpy).toHaveBeenCalled()
})
})

View File

@ -0,0 +1,98 @@
import { Component, OnInit } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { Subject, takeUntil } from 'rxjs'
import {
DATA_TYPE_LABELS,
PaperlessCustomField,
} from 'src/app/data/paperless-custom-field'
import { PermissionsService } from 'src/app/services/permissions.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { ToastService } from 'src/app/services/toast.service'
import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component'
@Component({
selector: 'pngx-custom-fields',
templateUrl: './custom-fields.component.html',
styleUrls: ['./custom-fields.component.scss'],
})
export class CustomFieldsComponent
extends ComponentWithPermissions
implements OnInit
{
public fields: PaperlessCustomField[] = []
private unsubscribeNotifier: Subject<any> = new Subject()
constructor(
private customFieldsService: CustomFieldsService,
public permissionsService: PermissionsService,
private modalService: NgbModal,
private toastService: ToastService
) {
super()
}
ngOnInit() {
this.reload()
}
reload() {
this.customFieldsService
.listAll()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((r) => {
this.fields = r.results
})
}
editField(field: PaperlessCustomField) {
const modal = this.modalService.open(CustomFieldEditDialogComponent)
modal.componentInstance.dialogMode = field
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.object = field
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newField) => {
this.toastService.showInfo($localize`Saved field "${newField.name}".`)
this.customFieldsService.clearCache()
this.reload()
})
modal.componentInstance.failed
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((e) => {
this.toastService.showError($localize`Error saving field.`, e)
})
}
deleteField(field: PaperlessCustomField) {
const modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.title = $localize`Confirm delete field`
modal.componentInstance.messageBold = $localize`This operation will permanently delete this field.`
modal.componentInstance.message = $localize`This operation cannot be undone.`
modal.componentInstance.btnClass = 'btn-danger'
modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.confirmClicked.subscribe(() => {
modal.componentInstance.buttonsEnabled = false
this.customFieldsService.delete(field).subscribe({
next: () => {
modal.close()
this.toastService.showInfo($localize`Deleted field`)
this.customFieldsService.clearCache()
this.reload()
},
error: (e) => {
this.toastService.showError($localize`Error deleting field.`, e)
},
})
})
}
getDataType(field: PaperlessCustomField): string {
return DATA_TYPE_LABELS.find((l) => l.id === field.data_type).name
}
}

View File

@ -4,13 +4,35 @@ export enum PaperlessCustomFieldDataType {
String = 'string', String = 'string',
Url = 'url', Url = 'url',
Date = 'date', Date = 'date',
Boolean = 'boolean',
Integer = 'integer',
} }
export const DATA_TYPE_LABELS = [
{
id: PaperlessCustomFieldDataType.Boolean,
name: $localize`Boolean`,
},
{
id: PaperlessCustomFieldDataType.Date,
name: $localize`Date`,
},
{
id: PaperlessCustomFieldDataType.Integer,
name: $localize`Number`,
},
{
id: PaperlessCustomFieldDataType.String,
name: $localize`String`,
},
{
id: PaperlessCustomFieldDataType.Url,
name: $localize`Url`,
},
]
export interface PaperlessCustomField extends ObjectWithId { export interface PaperlessCustomField extends ObjectWithId {
type: PaperlessCustomFieldDataType data_type: PaperlessCustomFieldDataType
name: string name: string
data: any
document: number // PaperlessDocument
created?: Date created?: Date
user: number // PaperlessUser
} }

View File

@ -26,6 +26,7 @@ export enum PermissionType {
Admin = '%s_logentry', Admin = '%s_logentry',
ShareLink = '%s_sharelink', ShareLink = '%s_sharelink',
ConsumptionTemplate = '%s_consumptiontemplate', ConsumptionTemplate = '%s_consumptiontemplate',
CustomField = '%s_customfield',
} }
@Injectable({ @Injectable({

View File

@ -9,24 +9,24 @@ import { PaperlessCustomField } from 'src/app/data/paperless-custom-field'
}) })
export class CustomFieldsService extends AbstractPaperlessService<PaperlessCustomField> { export class CustomFieldsService extends AbstractPaperlessService<PaperlessCustomField> {
constructor(http: HttpClient) { constructor(http: HttpClient) {
super(http, 'documents') super(http, 'custom_fields')
} }
getFields(documentId: number): Observable<PaperlessCustomField[]> { // getFields(documentId: number): Observable<PaperlessCustomField[]> {
return this.http.get<PaperlessCustomField[]>( // return this.http.get<PaperlessCustomField[]>(
this.getResourceUrl(documentId, 'custom_metadata') // this.getResourceUrl(documentId, 'custom_fields')
) // )
} // }
addField( // addField(
documentId: number, // documentId: number,
field: PaperlessCustomField // field: PaperlessCustomField
): Observable<PaperlessCustomField[]> { // ): Observable<PaperlessCustomField[]> {
return this.http.post<PaperlessCustomField[]>( // return this.http.post<PaperlessCustomField[]>(
this.getResourceUrl(documentId, 'custom_metadata'), // this.getResourceUrl(documentId, 'custom_fields'),
field // field
) // )
} // }
// deleteField( // deleteField(
// documentId: number, // documentId: number,