import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { DocumentListComponent } from './components/document-list/document-list.component'
import { CorrespondentListComponent } from './components/manage/correspondent-list/correspondent-list.component'
-import { CustomFieldsComponent } from './components/manage/custom-fields/custom-fields.component'
+import { CustomFieldsListComponent } from './components/manage/custom-fields-list/custom-fields-list.component'
import { DocumentTypeListComponent } from './components/manage/document-type-list/document-type-list.component'
import { MailComponent } from './components/manage/mail/mail.component'
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
},
{
path: 'customfields',
- component: CustomFieldsComponent,
+ component: CustomFieldsListComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-select i18n-title title="Data type" [items]="getDataTypes()" formControlName="data_type"></pngx-input-select>
@if (typeFieldDisabled) {
- <small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>
+ <small class="d-block mt-n2 fst-italic text-muted" i18n>Data type cannot be changed after a field is created</small>
}
<div [formGroup]="objectForm.controls.extra_data">
@switch (objectForm.get('data_type').value) {
}
}
</div>
+ <hr/>
+ <pngx-input-select i18n-title title="Matching algorithm" [items]="getMatchingAlgorithms()" formControlName="matching_algorithm"></pngx-input-select>
+ @if (patternRequired) {
+ <pngx-input-text i18n-title title="Matching pattern" formControlName="match" [error]="error?.match"></pngx-input-text>
+ }
+ @if (patternRequired) {
+ <pngx-input-check i18n-title title="Case insensitive" formControlName="is_insensitive" novalidate></pngx-input-check>
+ }
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
CustomFieldDataType,
DATA_TYPE_LABELS,
} from 'src/app/data/custom-field'
+import { MATCH_NONE } from 'src/app/data/matching-model'
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'
select_options: new FormArray([]),
default_currency: new FormControl(null),
}),
+ matching_algorithm: new FormControl(MATCH_NONE),
+ match: new FormControl(''),
+ is_insensitive: new FormControl(true),
})
}
--- /dev/null
+import { Component } from '@angular/core'
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
+import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
+import {
+ CustomFieldQueryLogicalOperator,
+ CustomFieldQueryOperator,
+} from 'src/app/data/custom-field-query'
+import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type'
+import { DocumentListViewService } from 'src/app/services/document-list-view.service'
+import {
+ PermissionsService,
+ PermissionType,
+} 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 { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
+import { ManagementListComponent } from '../management-list/management-list.component'
+
+@Component({
+ selector: 'pngx-custom-fields-list',
+ templateUrl: './../management-list/management-list.component.html',
+ styleUrls: ['./../management-list/management-list.component.scss'],
+})
+export class CustomFieldsListComponent extends ManagementListComponent<CustomField> {
+ permissionsDisabled = true
+
+ constructor(
+ customFieldsService: CustomFieldsService,
+ modalService: NgbModal,
+ toastService: ToastService,
+ documentListViewService: DocumentListViewService,
+ permissionsService: PermissionsService
+ ) {
+ super(
+ customFieldsService,
+ modalService,
+ CustomFieldEditDialogComponent,
+ toastService,
+ documentListViewService,
+ permissionsService,
+ 0, // see filterDocuments override below
+ $localize`custom field`,
+ $localize`custom fields`,
+ PermissionType.CustomField,
+ [
+ {
+ key: 'data_type',
+ name: $localize`Data Type`,
+ valueFn: (field: CustomField) => {
+ return DATA_TYPE_LABELS.find((l) => l.id === field.data_type).name
+ },
+ },
+ ]
+ )
+ }
+
+ filterDocuments(field: CustomField) {
+ this.documentListViewService.quickFilter([
+ {
+ rule_type: FILTER_CUSTOM_FIELDS_QUERY,
+ value: JSON.stringify([
+ CustomFieldQueryLogicalOperator.Or,
+ [[field.id, CustomFieldQueryOperator.Exists, true]],
+ ]),
+ },
+ ])
+ }
+
+ getDeleteMessage(object: CustomField) {
+ return $localize`Do you really want to delete the field "${object.name}"?`
+ }
+}
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 { PageHeaderComponent } from '../../common/page-header/page-header.component'
-import { CustomFieldsComponent } from './custom-fields.component'
+import { CustomFieldsComponent } from './custom-fields-list.component'
const fields: CustomField[] = [
{
+++ /dev/null
-<pngx-page-header
- title="Custom Fields"
- i18n-title
- info="Customize the data fields that can be attached to documents."
- i18n-info
- infoLink="usage/#custom-fields"
- >
- <button type="button" class="btn btn-sm btn-outline-primary" (click)="editField()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.CustomField }">
- <i-bs name="plus-circle"></i-bs> <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>
-
- @if (loading) {
- <li class="list-group-item">
- <div class="spinner-border spinner-border-sm me-2" role="status"></div>
- <ng-container i18n>Loading...</ng-container>
- </li>
- }
-
- @for (field of fields; track field) {
- <li class="list-group-item">
- <div class="row fade" [class.show]="show">
- <div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" 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 d-block d-sm-none">
- <div ngbDropdown container="body" class="d-inline-block">
- <button type="button" class="btn btn-link" id="actionsMenuMobile" (click)="$event.stopPropagation()" ngbDropdownToggle>
- <i-bs name="three-dots-vertical"></i-bs>
- </button>
- <div ngbDropdownMenu aria-labelledby="actionsMenuMobile">
- <button (click)="editField(field)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" ngbDropdownItem i18n>Edit</button>
- <button class="text-danger" (click)="deleteField(field)" *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.CustomField }" ngbDropdownItem i18n>Delete</button>
- @if (field.document_count > 0) {
- <button (click)="filterDocuments(field)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Document }" ngbDropdownItem i18n>Filter Documents ({{ field.document_count }})</button>
- }
- </div>
- </div>
- </div>
- <div class="btn-group d-none d-sm-inline-block">
- <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.CustomField }" class="btn btn-sm btn-outline-secondary" type="button" (click)="editField(field)">
- <i-bs width="1em" height="1em" name="pencil"></i-bs> <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)">
- <i-bs width="1em" height="1em" name="trash"></i-bs> <ng-container i18n>Delete</ng-container>
- </button>
- </div>
- @if (field.document_count > 0) {
- <div class="btn-group d-none d-sm-inline-block ms-2">
- <button class="btn btn-sm btn-outline-secondary" type="button" (click)="filterDocuments(field)">
- <i-bs width="1em" height="1em" name="filter"></i-bs> <ng-container i18n>Documents</ng-container><span class="badge bg-light text-secondary ms-2">{{ field.document_count }}</span>
- </button>
- </div>
- }
- </div>
- </div>
- </li>
- }
- @if (!loading && fields.length === 0) {
- <li class="list-group-item" i18n>No fields defined.</li>
- }
-</ul>
+++ /dev/null
-// hide caret on mobile dropdown
-.d-block.d-sm-none .dropdown-toggle::after {
- display: none;
-}
+++ /dev/null
-import { Component, OnInit } from '@angular/core'
-import {
- NgbDropdownModule,
- NgbModal,
- NgbPaginationModule,
-} from '@ng-bootstrap/ng-bootstrap'
-import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
-import { delay, takeUntil, tap } from 'rxjs'
-import { CustomField, DATA_TYPE_LABELS } from 'src/app/data/custom-field'
-import {
- CustomFieldQueryLogicalOperator,
- CustomFieldQueryOperator,
-} from 'src/app/data/custom-field-query'
-import { FILTER_CUSTOM_FIELDS_QUERY } from 'src/app/data/filter-rule-type'
-import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
-import { DocumentListViewService } from 'src/app/services/document-list-view.service'
-import { PermissionsService } from 'src/app/services/permissions.service'
-import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
-import { DocumentService } from 'src/app/services/rest/document.service'
-import { SavedViewService } from 'src/app/services/rest/saved-view.service'
-import { SettingsService } from 'src/app/services/settings.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 { PageHeaderComponent } from '../../common/page-header/page-header.component'
-import { LoadingComponentWithPermissions } from '../../loading-component/loading.component'
-
-@Component({
- selector: 'pngx-custom-fields',
- templateUrl: './custom-fields.component.html',
- styleUrls: ['./custom-fields.component.scss'],
- imports: [
- PageHeaderComponent,
- IfPermissionsDirective,
- NgbDropdownModule,
- NgbPaginationModule,
- NgxBootstrapIconsModule,
- ],
-})
-export class CustomFieldsComponent
- extends LoadingComponentWithPermissions
- implements OnInit
-{
- public fields: CustomField[] = []
-
- constructor(
- private customFieldsService: CustomFieldsService,
- public permissionsService: PermissionsService,
- private modalService: NgbModal,
- private toastService: ToastService,
- private documentListViewService: DocumentListViewService,
- private settingsService: SettingsService,
- private documentService: DocumentService,
- private savedViewService: SavedViewService
- ) {
- super()
- }
-
- ngOnInit() {
- this.reload()
- }
-
- reload() {
- this.customFieldsService
- .listAll()
- .pipe(
- takeUntil(this.unsubscribeNotifier),
- tap((r) => {
- this.fields = r.results
- }),
- delay(100)
- )
- .subscribe(() => {
- this.show = true
- this.loading = false
- })
- }
-
- editField(field: CustomField) {
- 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.settingsService.initializeDisplayFields()
- this.documentService.reload()
- this.reload()
- })
- modal.componentInstance.failed
- .pipe(takeUntil(this.unsubscribeNotifier))
- .subscribe((e) => {
- this.toastService.showError($localize`Error saving field.`, e)
- })
- }
-
- deleteField(field: CustomField) {
- 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 "${field.name}"`)
- this.customFieldsService.clearCache()
- this.settingsService.initializeDisplayFields()
- this.documentService.reload()
- this.savedViewService.reload()
- this.reload()
- },
- error: (e) => {
- this.toastService.showError(
- $localize`Error deleting field "${field.name}".`,
- e
- )
- },
- })
- })
- }
-
- getDataType(field: CustomField): string {
- return DATA_TYPE_LABELS.find((l) => l.id === field.data_type).name
- }
-
- filterDocuments(field: CustomField) {
- this.documentListViewService.quickFilter([
- {
- rule_type: FILTER_CUSTOM_FIELDS_QUERY,
- value: JSON.stringify([
- CustomFieldQueryLogicalOperator.Or,
- [[field.id, CustomFieldQueryOperator.Exists, true]],
- ]),
- },
- ])
- }
-}
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
<i-bs name="x"></i-bs> <ng-container i18n>Clear selection</ng-container>
</button>
- <button type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
+ <button *ngIf="!permissionsDisabled" type="button" class="btn btn-sm btn-outline-primary" (click)="setPermissions()" [disabled]="!userCanBulkEdit(PermissionAction.Change) || selectedObjects.size === 0">
<i-bs name="person-fill-lock"></i-bs> <ng-container i18n>Permissions</ng-container>
</button>
<button type="button" class="btn btn-sm btn-outline-danger" (click)="delete()" [disabled]="!userCanBulkEdit(PermissionAction.Delete) || selectedObjects.size === 0">
private modalService: NgbModal,
private editDialogComponent: any,
private toastService: ToastService,
- private documentListViewService: DocumentListViewService,
+ protected documentListViewService: DocumentListViewService,
private permissionsService: PermissionsService,
protected filterRuleType: number,
public typeName: string,
public selectedObjects: Set<number> = new Set()
public togggleAll: boolean = false
+ protected permissionsDisabled: boolean = false
+
ngOnInit(): void {
this.reloadData()
-import { ObjectWithId } from './object-with-id'
+import { MatchingModel } from './matching-model'
export enum CustomFieldDataType {
String = 'string',
},
]
-export interface CustomField extends ObjectWithId {
+export interface CustomField extends MatchingModel {
data_type: CustomFieldDataType
- name: string
created?: Date
extra_data?: {
select_options?: Array<{ label: string; id: string }>
default_currency?: string
}
- document_count?: number
}
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { CustomField } from 'src/app/data/custom-field'
-import { AbstractPaperlessService } from './abstract-paperless-service'
+import { AbstractNameFilterService } from './abstract-name-filter-service'
@Injectable({
providedIn: 'root',
})
-export class CustomFieldsService extends AbstractPaperlessService<CustomField> {
+export class CustomFieldsService extends AbstractNameFilterService<CustomField> {
constructor(http: HttpClient) {
super(http, 'custom_fields')
}