Make frontend list a generic management list
This commit is contained in:
		
							parent
							
								
									7a46806643
								
							
						
					
					
						commit
						b8c618abbe
					
				| @ -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: { | ||||
|  | ||||
| @ -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) { | ||||
| @ -39,6 +39,14 @@ | ||||
|         } | ||||
|       } | ||||
|     </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> | ||||
|  | ||||
| @ -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), | ||||
|     }) | ||||
|   } | ||||
| 
 | ||||
|  | ||||
| @ -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}"?` | ||||
|   } | ||||
| } | ||||
| @ -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[] = [ | ||||
|   { | ||||
| @ -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> <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> | ||||
| @ -1,4 +0,0 @@ | ||||
| // hide caret on mobile dropdown | ||||
| .d-block.d-sm-none .dropdown-toggle::after { | ||||
|     display: none; | ||||
| } | ||||
| @ -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]], | ||||
|         ]), | ||||
|       }, | ||||
|     ]) | ||||
|   } | ||||
| } | ||||
| @ -2,7 +2,7 @@ | ||||
|   <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"> | ||||
|  | ||||
| @ -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() | ||||
| 
 | ||||
|  | ||||
| @ -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 | ||||
| } | ||||
|  | ||||
| @ -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') | ||||
|   } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user