]> git.ipfire.org Git - thirdparty/paperless-ngx.git/commitdiff
Make frontend list a generic management list
authorshamoon <4887959+shamoon@users.noreply.github.com>
Fri, 13 Dec 2024 21:24:39 +0000 (13:24 -0800)
committershamoon <4887959+shamoon@users.noreply.github.com>
Thu, 20 Mar 2025 23:21:14 +0000 (16:21 -0700)
12 files changed:
src-ui/src/app/app-routing.module.ts
src-ui/src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html
src-ui/src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts
src-ui/src/app/components/manage/custom-fields-list/custom-fields-list.component.ts [new file with mode: 0644]
src-ui/src/app/components/manage/custom-fields-list/custom-fields.component.spec.ts [moved from src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts with 98% similarity]
src-ui/src/app/components/manage/custom-fields/custom-fields.component.html [deleted file]
src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss [deleted file]
src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts [deleted file]
src-ui/src/app/components/manage/management-list/management-list.component.html
src-ui/src/app/components/manage/management-list/management-list.component.ts
src-ui/src/app/data/custom-field.ts
src-ui/src/app/services/rest/custom-fields.service.ts

index f65514f74450219bd83b2bcfd9e7b21bf0625234..9d97bcde3bea87f9c22f4210d95af3f9227bbe94 100644 (file)
@@ -12,7 +12,7 @@ import { DocumentAsnComponent } from './components/document-asn/document-asn.com
 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'
@@ -239,7 +239,7 @@ export const routes: Routes = [
       },
       {
         path: 'customfields',
-        component: CustomFieldsComponent,
+        component: CustomFieldsListComponent,
         canActivate: [PermissionsGuard],
         data: {
           requiredPermission: {
index b4216e41c6ad99dc07e17d4bfab3d1e1e9f1f47d..af194c0151e19f2e34e478fb971c4b858097d7c2 100644 (file)
@@ -11,7 +11,7 @@
     <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>
index 384bf7b6805d8895e9850244d351d4052040eac3..fada6c53e0e099c2d7e91a43d6e68ec3ab409de8 100644 (file)
@@ -21,6 +21,7 @@ import {
   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'
@@ -107,6 +108,9 @@ export class CustomFieldEditDialogComponent
         select_options: new FormArray([]),
         default_currency: new FormControl(null),
       }),
+      matching_algorithm: new FormControl(MATCH_NONE),
+      match: new FormControl(''),
+      is_insensitive: new FormControl(true),
     })
   }
 
diff --git a/src-ui/src/app/components/manage/custom-fields-list/custom-fields-list.component.ts b/src-ui/src/app/components/manage/custom-fields-list/custom-fields-list.component.ts
new file mode 100644 (file)
index 0000000..daa9bb3
--- /dev/null
@@ -0,0 +1,72 @@
+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}"?`
+  }
+}
similarity index 98%
rename from src-ui/src/app/components/manage/custom-fields/custom-fields.component.spec.ts
rename to src-ui/src/app/components/manage/custom-fields-list/custom-fields.component.spec.ts
index e94470d64c797474529200254b28269f3aa33533..30ecf947201829b2fceae380b8bf6aa3aaca4c19 100644 (file)
@@ -28,7 +28,7 @@ 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 { PageHeaderComponent } from '../../common/page-header/page-header.component'
-import { CustomFieldsComponent } from './custom-fields.component'
+import { CustomFieldsComponent } from './custom-fields-list.component'
 
 const fields: CustomField[] = [
   {
diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.html
deleted file mode 100644 (file)
index 185e9da..0000000
+++ /dev/null
@@ -1,72 +0,0 @@
-<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>&nbsp;<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>&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)">
-              <i-bs width="1em" height="1em" name="trash"></i-bs>&nbsp;<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>&nbsp;<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>
diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.scss
deleted file mode 100644 (file)
index dfdd204..0000000
+++ /dev/null
@@ -1,4 +0,0 @@
-// hide caret on mobile dropdown
-.d-block.d-sm-none .dropdown-toggle::after {
-    display: none;
-}
diff --git a/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts b/src-ui/src/app/components/manage/custom-fields/custom-fields.component.ts
deleted file mode 100644 (file)
index b4fd973..0000000
+++ /dev/null
@@ -1,148 +0,0 @@
-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]],
-        ]),
-      },
-    ])
-  }
-}
index 82fd8502c9db1eb19a59d34bbfcd6e21bf9ede38..a8c6101c1557767fa039eb479ccd5bc78f0c9b28 100644 (file)
@@ -2,7 +2,7 @@
   <button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedObjects.size === 0">
     <i-bs  name="x"></i-bs>&nbsp;<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>&nbsp;<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">
index 7f7721485b48d954842629a32088d2d031fbd953..340de55dfc9e4dd958bade4db1fd208181d95087 100644 (file)
@@ -64,7 +64,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
     private modalService: NgbModal,
     private editDialogComponent: any,
     private toastService: ToastService,
-    private documentListViewService: DocumentListViewService,
+    protected documentListViewService: DocumentListViewService,
     private permissionsService: PermissionsService,
     protected filterRuleType: number,
     public typeName: string,
@@ -93,6 +93,8 @@ export abstract class ManagementListComponent<T extends MatchingModel>
   public selectedObjects: Set<number> = new Set()
   public togggleAll: boolean = false
 
+  protected permissionsDisabled: boolean = false
+
   ngOnInit(): void {
     this.reloadData()
 
index c5130a756ff338048184557973c41d1c6a332885..20d7e3c481bb8ac3b9fb5b1600dea35a87d6fd00 100644 (file)
@@ -1,4 +1,4 @@
-import { ObjectWithId } from './object-with-id'
+import { MatchingModel } from './matching-model'
 
 export enum CustomFieldDataType {
   String = 'string',
@@ -51,13 +51,11 @@ export const DATA_TYPE_LABELS = [
   },
 ]
 
-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
 }
index 0ac31eefd5b947bd37f90d55f1c15538a0aefb84..aed14d83193ba043eeef17935007a5c342ad732f 100644 (file)
@@ -1,12 +1,12 @@
 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')
   }