mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Feature: consumption templates (#4196)
* Initial implementation of consumption templates * Frontend implementation of consumption templates Testing * Support consumption template source * order templates, automatically add permissions * Support title assignment in consumption templates * Refactoring, filters to and, show sources on list Show sources on template list, update some translation strings Make filters and minor testing * Update strings * Only update django-multiselectfield * Basic docs, document some methods * Improve testing coverage, template multi-assignment merges
This commit is contained in:
		| @@ -21,6 +21,7 @@ import { | ||||
|   PermissionAction, | ||||
|   PermissionType, | ||||
| } from './services/permissions.service' | ||||
| import { ConsumptionTemplatesListComponent } from './components/manage/consumption-templates-list/consumption-templates-list.component' | ||||
|  | ||||
| export const routes: Routes = [ | ||||
|   { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, | ||||
| @@ -182,7 +183,17 @@ export const routes: Routes = [ | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|       { path: 'tasks', component: TasksComponent }, | ||||
|       { | ||||
|         path: 'templates', | ||||
|         component: ConsumptionTemplatesListComponent, | ||||
|         canActivate: [PermissionsGuard], | ||||
|         data: { | ||||
|           requiredPermission: { | ||||
|             action: PermissionAction.View, | ||||
|             type: PermissionType.ConsumptionTemplate, | ||||
|           }, | ||||
|         }, | ||||
|       }, | ||||
|     ], | ||||
|   }, | ||||
|  | ||||
|   | ||||
| @@ -95,6 +95,8 @@ import { UsernamePipe } from './pipes/username.pipe' | ||||
| import { LogoComponent } from './components/common/logo/logo.component' | ||||
| import { IsNumberPipe } from './pipes/is-number.pipe' | ||||
| import { ShareLinksDropdownComponent } from './components/common/share-links-dropdown/share-links-dropdown.component' | ||||
| import { ConsumptionTemplatesListComponent } from './components/manage/consumption-templates-list/consumption-templates-list.component' | ||||
| import { ConsumptionTemplateEditDialogComponent } from './components/common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component' | ||||
|  | ||||
| import localeAf from '@angular/common/locales/af' | ||||
| import localeAr from '@angular/common/locales/ar' | ||||
| @@ -233,6 +235,8 @@ function initializeApp(settings: SettingsService) { | ||||
|     LogoComponent, | ||||
|     IsNumberPipe, | ||||
|     ShareLinksDropdownComponent, | ||||
|     ConsumptionTemplatesListComponent, | ||||
|     ConsumptionTemplateEditDialogComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|   | ||||
| @@ -155,6 +155,13 @@ | ||||
|               </svg><span> <ng-container i18n>Storage paths</ng-container></span> | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.ConsumptionTemplate }"> | ||||
|             <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"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#file-earmark-ruled"/> | ||||
|               </svg><span> <ng-container i18n>Templates</ng-container></span> | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.PaperlessTask }" tourAnchor="tour.file-tasks"> | ||||
|             <a class="nav-link" routerLink="tasks" routerLinkActive="active" (click)="closeMenu()" ngbPopover="File Tasks" i18n-ngbPopover [disablePopover]="!slimSidebarEnabled" placement="end" container="body" triggers="mouseenter:mouseleave" popoverClass="popover-slim"> | ||||
|               <span *ngIf="tasksService.failedFileTasks.length > 0 && slimSidebarEnabled" class="badge bg-danger position-absolute top-0 end-0">{{tasksService.failedFileTasks.length}}</span> | ||||
|   | ||||
| @@ -0,0 +1,92 @@ | ||||
| <form [formGroup]="objectForm" (ngSubmit)="save()"> | ||||
|     <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"> | ||||
|       <div class="row"> | ||||
|         <div class="col-md-8"> | ||||
|           <pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></pngx-input-text> | ||||
|         </div> | ||||
|         <div class="col"> | ||||
|           <pngx-input-number i18n-title title="Sort order" formControlName="order" [showAdd]="false" [error]="error?.order"></pngx-input-number> | ||||
|         </div> | ||||
|       </div> | ||||
|       <div class="row"> | ||||
|         <div class="col-md-4"> | ||||
|           <h5 class="border-bottom pb-2" i18n>Filters</h5> | ||||
|           <p class="small" i18n>Process documents that match <em>all</em> filters specified below.</p> | ||||
|           <pngx-input-select i18n-title title="Filter sources" [items]="sourceOptions" [multiple]="true" formControlName="sources" [error]="error?.filter_filename"></pngx-input-select> | ||||
|           <pngx-input-text i18n-title title="Filter filename" formControlName="filter_filename" i18n-hint hint="Apply to documents that match this filename. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_filename"></pngx-input-text> | ||||
|           <pngx-input-text i18n-title title="Filter path" formControlName="filter_path" i18n-hint hint="Apply to documents that match this path. Wildcards specified as * are allowed. Case insensitive.</a>" [error]="error?.filter_path"></pngx-input-text> | ||||
|           <pngx-input-select i18n-title title="Filter mail rule" [items]="mailRules" [allowNull]="true" formControlName="filter_mailrule" i18n-hint hint="Apply to documents consumed via this mail rule." [error]="error?.filter_mailrule"></pngx-input-select> | ||||
|         </div> | ||||
|         <div class="col"> | ||||
|           <div class="row"> | ||||
|             <div class="col"> | ||||
|               <h5 class="border-bottom pb-2" i18n>Assignments</h5> | ||||
|             </div> | ||||
|           </div> | ||||
|           <div class="row"> | ||||
|             <div class="col"> | ||||
|               <pngx-input-text i18n-title title="Assign title" formControlName="assign_title" i18n-hint hint="Can include some placeholders, see <a target='_blank' href='https://docs.paperless-ngx.com/usage/#consumption-templates'>documentation</a>." [error]="error?.assign_title"></pngx-input-text> | ||||
|               <pngx-input-tags [allowCreate]="false" i18n-title title="Assign tags" formControlName="assign_tags"></pngx-input-tags> | ||||
|               <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select> | ||||
|               <pngx-input-select i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select> | ||||
|               <pngx-input-select i18n-title title="Assign storage path" [items]="storagePaths" [allowNull]="true" formControlName="assign_storage_path"></pngx-input-select> | ||||
|           </div> | ||||
|           <div class="col"> | ||||
|             <pngx-input-select i18n-title title="Assign owner" [items]="users" bindLabel="username" formControlName="assign_owner" [allowNull]="true"></pngx-input-select> | ||||
|               <div> | ||||
|                 <label class="form-label" i18n>Assign view permissions</label> | ||||
|                 <div class="mb-2"> | ||||
|                   <div class="row mb-1"> | ||||
|                     <div class="col-lg-3"> | ||||
|                       <label class="form-label d-block my-2" i18n>Users:</label> | ||||
|                     </div> | ||||
|                     <div class="col-lg-9"> | ||||
|                       <pngx-permissions-user type="view" formControlName="assign_view_users"></pngx-permissions-user> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div class="row"> | ||||
|                     <div class="col-lg-3"> | ||||
|                       <label class="form-label d-block my-2" i18n>Groups:</label> | ||||
|                     </div> | ||||
|                     <div class="col-lg-9"> | ||||
|                       <pngx-permissions-group type="view" formControlName="assign_view_groups"></pngx-permissions-group> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <label class="form-label" i18n>Assign edit permissions</label> | ||||
|                 <div> | ||||
|                   <div class="row mb-1"> | ||||
|                     <div class="col-lg-3"> | ||||
|                       <label class="form-label d-block my-2" i18n>Users:</label> | ||||
|                     </div> | ||||
|                     <div class="col-lg-9"> | ||||
|                       <pngx-permissions-user type="change" formControlName="assign_change_users"></pngx-permissions-user> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <div class="row"> | ||||
|                     <div class="col-lg-3"> | ||||
|                       <label class="form-label d-block my-2" i18n>Groups:</label> | ||||
|                     </div> | ||||
|                     <div class="col-lg-9"> | ||||
|                       <pngx-permissions-group type="change" formControlName="assign_change_groups"></pngx-permissions-group> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                   <small class="form-text text-muted text-end d-block" i18n>Edit permissions also grant viewing permissions</small> | ||||
|                 </div> | ||||
|               </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|       </div> | ||||
|     </div> | ||||
|     <div class="modal-footer"> | ||||
|       <span class="text-danger" *ngIf="error?.non_field_errors"><ng-container i18n>Error</ng-container>: {{error.non_field_errors}}</span> | ||||
|       <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> | ||||
| @@ -0,0 +1,125 @@ | ||||
| import { HttpClientTestingModule } from '@angular/common/http/testing' | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing' | ||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms' | ||||
| import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { NgSelectModule } from '@ng-select/ng-select' | ||||
| import { of } from 'rxjs' | ||||
| 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 { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||
| import { MailRuleService } from 'src/app/services/rest/mail-rule.service' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { NumberComponent } from '../../input/number/number.component' | ||||
| import { PermissionsGroupComponent } from '../../input/permissions/permissions-group/permissions-group.component' | ||||
| import { PermissionsUserComponent } from '../../input/permissions/permissions-user/permissions-user.component' | ||||
| import { SelectComponent } from '../../input/select/select.component' | ||||
| import { TagsComponent } from '../../input/tags/tags.component' | ||||
| import { TextComponent } from '../../input/text/text.component' | ||||
| import { EditDialogMode } from '../edit-dialog.component' | ||||
| import { ConsumptionTemplateEditDialogComponent } from './consumption-template-edit-dialog.component' | ||||
|  | ||||
| describe('ConsumptionTemplateEditDialogComponent', () => { | ||||
|   let component: ConsumptionTemplateEditDialogComponent | ||||
|   let settingsService: SettingsService | ||||
|   let fixture: ComponentFixture<ConsumptionTemplateEditDialogComponent> | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [ | ||||
|         ConsumptionTemplateEditDialogComponent, | ||||
|         IfPermissionsDirective, | ||||
|         IfOwnerDirective, | ||||
|         SelectComponent, | ||||
|         TextComponent, | ||||
|         NumberComponent, | ||||
|         TagsComponent, | ||||
|         PermissionsUserComponent, | ||||
|         PermissionsGroupComponent, | ||||
|         SafeHtmlPipe, | ||||
|       ], | ||||
|       providers: [ | ||||
|         NgbActiveModal, | ||||
|         { | ||||
|           provide: CorrespondentService, | ||||
|           useValue: { | ||||
|             listAll: () => | ||||
|               of({ | ||||
|                 results: [ | ||||
|                   { | ||||
|                     id: 1, | ||||
|                     username: 'c1', | ||||
|                   }, | ||||
|                 ], | ||||
|               }), | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           provide: DocumentTypeService, | ||||
|           useValue: { | ||||
|             listAll: () => | ||||
|               of({ | ||||
|                 results: [ | ||||
|                   { | ||||
|                     id: 1, | ||||
|                     username: 'dt1', | ||||
|                   }, | ||||
|                 ], | ||||
|               }), | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           provide: StoragePathService, | ||||
|           useValue: { | ||||
|             listAll: () => | ||||
|               of({ | ||||
|                 results: [ | ||||
|                   { | ||||
|                     id: 1, | ||||
|                     username: 'sp1', | ||||
|                   }, | ||||
|                 ], | ||||
|               }), | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           provide: MailRuleService, | ||||
|           useValue: { | ||||
|             listAll: () => | ||||
|               of({ | ||||
|                 results: [], | ||||
|               }), | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|       imports: [ | ||||
|         HttpClientTestingModule, | ||||
|         FormsModule, | ||||
|         ReactiveFormsModule, | ||||
|         NgSelectModule, | ||||
|         NgbModule, | ||||
|       ], | ||||
|     }).compileComponents() | ||||
|  | ||||
|     fixture = TestBed.createComponent(ConsumptionTemplateEditDialogComponent) | ||||
|     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() | ||||
|   }) | ||||
| }) | ||||
| @@ -0,0 +1,115 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { FormGroup, FormControl } from '@angular/forms' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { first } from 'rxjs' | ||||
| import { | ||||
|   DocumentSource, | ||||
|   PaperlessConsumptionTemplate, | ||||
| } from 'src/app/data/paperless-consumption-template' | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service' | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| import { UserService } from 'src/app/services/rest/user.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { EditDialogComponent } from '../edit-dialog.component' | ||||
| import { MailRuleService } from 'src/app/services/rest/mail-rule.service' | ||||
| import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule' | ||||
|  | ||||
| export const DOCUMENT_SOURCE_OPTIONS = [ | ||||
|   { | ||||
|     id: DocumentSource.ConsumeFolder, | ||||
|     name: $localize`Consume Folder`, | ||||
|   }, | ||||
|   { | ||||
|     id: DocumentSource.ApiUpload, | ||||
|     name: $localize`API Upload`, | ||||
|   }, | ||||
|   { | ||||
|     id: DocumentSource.MailFetch, | ||||
|     name: $localize`Mail Fetch`, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'pngx-consumption-template-edit-dialog', | ||||
|   templateUrl: './consumption-template-edit-dialog.component.html', | ||||
|   styleUrls: ['./consumption-template-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class ConsumptionTemplateEditDialogComponent extends EditDialogComponent<PaperlessConsumptionTemplate> { | ||||
|   templates: PaperlessConsumptionTemplate[] | ||||
|   correspondents: PaperlessCorrespondent[] | ||||
|   documentTypes: PaperlessDocumentType[] | ||||
|   storagePaths: PaperlessStoragePath[] | ||||
|   mailRules: PaperlessMailRule[] | ||||
|  | ||||
|   constructor( | ||||
|     service: ConsumptionTemplateService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     correspondentService: CorrespondentService, | ||||
|     documentTypeService: DocumentTypeService, | ||||
|     storagePathService: StoragePathService, | ||||
|     mailRuleService: MailRuleService, | ||||
|     userService: UserService, | ||||
|     settingsService: SettingsService | ||||
|   ) { | ||||
|     super(service, activeModal, userService, settingsService) | ||||
|  | ||||
|     correspondentService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.correspondents = result.results)) | ||||
|  | ||||
|     documentTypeService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.documentTypes = result.results)) | ||||
|  | ||||
|     storagePathService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.storagePaths = result.results)) | ||||
|  | ||||
|     mailRuleService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.mailRules = result.results)) | ||||
|   } | ||||
|  | ||||
|   getCreateTitle() { | ||||
|     return $localize`Create new consumption template` | ||||
|   } | ||||
|  | ||||
|   getEditTitle() { | ||||
|     return $localize`Edit consumption template` | ||||
|   } | ||||
|  | ||||
|   getForm(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(null), | ||||
|       account: new FormControl(null), | ||||
|       filter_filename: new FormControl(null), | ||||
|       filter_path: new FormControl(null), | ||||
|       filter_mailrule: new FormControl(null), | ||||
|       order: new FormControl(null), | ||||
|       sources: new FormControl([]), | ||||
|       assign_title: new FormControl(null), | ||||
|       assign_tags: new FormControl([]), | ||||
|       assign_owner: new FormControl(null), | ||||
|       assign_document_type: new FormControl(null), | ||||
|       assign_correspondent: new FormControl(null), | ||||
|       assign_storage_path: new FormControl(null), | ||||
|       assign_view_users: new FormControl([]), | ||||
|       assign_view_groups: new FormControl([]), | ||||
|       assign_change_users: new FormControl([]), | ||||
|       assign_change_groups: new FormControl([]), | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   get sourceOptions() { | ||||
|     return DOCUMENT_SOURCE_OPTIONS | ||||
|   } | ||||
| } | ||||
| @@ -26,11 +26,13 @@ | ||||
|       <div class="col-md-4"> | ||||
|         <pngx-input-select i18n-title title="Action" [items]="actionOptions" formControlName="action" i18n-hint hint="Action is only performed when documents are consumed from the mail. Mails without attachments remain entirely untouched."></pngx-input-select> | ||||
|         <pngx-input-text i18n-title title="Action parameter" *ngIf="showActionParamField" formControlName="action_parameter" [error]="error?.action_parameter"></pngx-input-text> | ||||
|         <p class="small fst-italic mt-5" i18n>Assignments specified here will supersede any consumption templates.</p> | ||||
|         <pngx-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></pngx-input-select> | ||||
|         <pngx-input-tags [allowCreate]="false" formControlName="assign_tags"></pngx-input-tags> | ||||
|         <pngx-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select> | ||||
|         <pngx-input-select i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></pngx-input-select> | ||||
|         <pngx-input-select *ngIf="showCorrespondentField" i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select> | ||||
|         <pngx-input-check i18n-title title="Assign owner from rule" formControlName="assign_owner_from_rule"></pngx-input-check> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import { CorrespondentService } from 'src/app/services/rest/correspondent.servic | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||
| import { MailAccountService } from 'src/app/services/rest/mail-account.service' | ||||
| import { SettingsService } from 'src/app/services/settings.service' | ||||
| import { CheckComponent } from '../../input/check/check.component' | ||||
| import { NumberComponent } from '../../input/number/number.component' | ||||
| import { PermissionsFormComponent } from '../../input/permissions/permissions-form/permissions-form.component' | ||||
| import { SelectComponent } from '../../input/select/select.component' | ||||
| @@ -27,9 +28,6 @@ describe('MailRuleEditDialogComponent', () => { | ||||
|   let component: MailRuleEditDialogComponent | ||||
|   let settingsService: SettingsService | ||||
|   let fixture: ComponentFixture<MailRuleEditDialogComponent> | ||||
|   let accountService: MailAccountService | ||||
|   let correspondentService: CorrespondentService | ||||
|   let documentTypeService: DocumentTypeService | ||||
|  | ||||
|   beforeEach(async () => { | ||||
|     TestBed.configureTestingModule({ | ||||
| @@ -43,6 +41,7 @@ describe('MailRuleEditDialogComponent', () => { | ||||
|         NumberComponent, | ||||
|         TagsComponent, | ||||
|         SafeHtmlPipe, | ||||
|         CheckComponent, | ||||
|       ], | ||||
|       providers: [ | ||||
|         NgbActiveModal, | ||||
|   | ||||
| @@ -79,6 +79,10 @@ const METADATA_TITLE_OPTIONS = [ | ||||
|     id: MailMetadataTitleOption.FromFilename, | ||||
|     name: $localize`Use attachment filename as title`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailMetadataTitleOption.None, | ||||
|     name: $localize`Do not assign title from this rule`, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const METADATA_CORRESPONDENT_OPTIONS = [ | ||||
| @@ -168,6 +172,7 @@ export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMa | ||||
|         MailMetadataCorrespondentOption.FromNothing | ||||
|       ), | ||||
|       assign_correspondent: new FormControl(null), | ||||
|       assign_owner_from_rule: new FormControl(true), | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ | ||||
|     </div> | ||||
|     <div class="modal-body"> | ||||
|       <div class="row"> | ||||
|         <div class="col"> | ||||
|         <div class="col-md-4"> | ||||
|           <pngx-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></pngx-input-text> | ||||
|           <pngx-input-text i18n-title title="Email" formControlName="email" [error]="error?.email"></pngx-input-text> | ||||
|           <pngx-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></pngx-input-password> | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| <div class="mb-3 paperless-input-select paperless-input-tags" [class.disabled]="disabled"> | ||||
|   <label class="form-label" for="tags" i18n>Tags</label> | ||||
|   <label class="form-label" for="tags" i18n>{{title}}</label> | ||||
|  | ||||
|   <div class="input-group flex-nowrap"> | ||||
|     <ng-select #tagSelect name="tags" [items]="tags" bindLabel="name" bindValue="id" [(ngModel)]="value" | ||||
| @@ -7,7 +7,7 @@ | ||||
|       [multiple]="true" | ||||
|       [closeOnSelect]="false" | ||||
|       [clearSearchOnAdd]="true" | ||||
|       [hideSelected]="true" | ||||
|       [hideSelected]="tags.length > 0" | ||||
|       [addTag]="allowCreate ? createTagRef : false" | ||||
|       addTagText="Add tag" | ||||
|       i18n-addTagText | ||||
|   | ||||
| @@ -59,6 +59,9 @@ export class TagsComponent implements OnInit, ControlValueAccessor { | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   @Input() | ||||
|   title = $localize`Tags` | ||||
|  | ||||
|   @Input() | ||||
|   disabled = false | ||||
|  | ||||
|   | ||||
| @@ -0,0 +1,33 @@ | ||||
| <pngx-page-header title="Consumption Templates"> | ||||
|     <button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editTemplate()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.ConsumptionTemplate }"> | ||||
|       <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|       </svg> | ||||
|       <ng-container i18n>Add Template</ng-container> | ||||
|     </button> | ||||
|   </pngx-page-header> | ||||
|  | ||||
|   <table class="table table-striped align-middle border shadow-sm"> | ||||
|     <thead> | ||||
|         <tr> | ||||
|             <th scope="col" i18n>Name</th> | ||||
|             <th scope="col" i18n>Sort order</th> | ||||
|             <th scope="col" i18n>Document Sources</th> | ||||
|             <th scope="col" i18n>Actions</th> | ||||
|         </tr> | ||||
|     </thead> | ||||
|     <tbody> | ||||
|         <tr *ngFor="let template of templates"> | ||||
|             <td scope="row"><button class="btn btn-link p-0" type="button" (click)="editTemplate(template)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.ConsumptionTemplate)">{{template.name}}</button></td> | ||||
|             <td scope="row"><code>{{template.order}}</code></td> | ||||
|             <td scope="row">{{getSourceList(template)}}</td> | ||||
|             <td scope="row"> | ||||
|                 <div class="btn-group"> | ||||
|                     <button *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-primary" type="button" (click)="editTemplate(template)" i18n>Edit</button> | ||||
|                     <button *pngxIfPermissions="{ action: PermissionAction.Delete, type: PermissionType.ConsumptionTemplate }" class="btn btn-sm btn-outline-danger" type="button" (click)="deleteTemplate(template)" i18n>Delete</button> | ||||
|                 </div> | ||||
|             </td> | ||||
|         </tr> | ||||
|     </tbody> | ||||
| </table> | ||||
| <div *ngIf="templates.length === 0" i18n>No templates defined.</div> | ||||
| @@ -0,0 +1,175 @@ | ||||
| import { HttpClientTestingModule } from '@angular/common/http/testing' | ||||
| import { ComponentFixture, TestBed } from '@angular/core/testing' | ||||
| import { FormsModule, ReactiveFormsModule } from '@angular/forms' | ||||
| import { By } from '@angular/platform-browser' | ||||
| import { | ||||
|   NgbModal, | ||||
|   NgbPaginationModule, | ||||
|   NgbModalRef, | ||||
|   NgbModalModule, | ||||
| } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { of, throwError } from 'rxjs' | ||||
| import { | ||||
|   DocumentSource, | ||||
|   PaperlessConsumptionTemplate, | ||||
| } from 'src/app/data/paperless-consumption-template' | ||||
| import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive' | ||||
| import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.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 { ConsumptionTemplatesListComponent } from './consumption-templates-list.component' | ||||
| import { ConsumptionTemplateEditDialogComponent } from '../../common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component' | ||||
| import { PermissionsService } from 'src/app/services/permissions.service' | ||||
|  | ||||
| const templates: PaperlessConsumptionTemplate[] = [ | ||||
|   { | ||||
|     id: 0, | ||||
|     name: 'Template 1', | ||||
|     order: 0, | ||||
|     sources: [ | ||||
|       DocumentSource.ConsumeFolder, | ||||
|       DocumentSource.ApiUpload, | ||||
|       DocumentSource.MailFetch, | ||||
|     ], | ||||
|     filter_filename: 'foo', | ||||
|     filter_path: 'bar', | ||||
|     assign_tags: [1, 2, 3], | ||||
|   }, | ||||
|   { | ||||
|     id: 1, | ||||
|     name: 'Template 2', | ||||
|     order: 1, | ||||
|     sources: [DocumentSource.MailFetch], | ||||
|     filter_filename: null, | ||||
|     filter_path: 'foo/bar', | ||||
|     assign_owner: 1, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| describe('ConsumptionTemplatesComponent', () => { | ||||
|   let component: ConsumptionTemplatesListComponent | ||||
|   let fixture: ComponentFixture<ConsumptionTemplatesListComponent> | ||||
|   let consumptionTemplateService: ConsumptionTemplateService | ||||
|   let modalService: NgbModal | ||||
|   let toastService: ToastService | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     TestBed.configureTestingModule({ | ||||
|       declarations: [ | ||||
|         ConsumptionTemplatesListComponent, | ||||
|         IfPermissionsDirective, | ||||
|         PageHeaderComponent, | ||||
|         ConfirmDialogComponent, | ||||
|       ], | ||||
|       providers: [ | ||||
|         { | ||||
|           provide: PermissionsService, | ||||
|           useValue: { | ||||
|             currentUserCan: () => true, | ||||
|             currentUserHasObjectPermissions: () => true, | ||||
|             currentUserOwnsObject: () => true, | ||||
|           }, | ||||
|         }, | ||||
|       ], | ||||
|       imports: [ | ||||
|         HttpClientTestingModule, | ||||
|         NgbPaginationModule, | ||||
|         FormsModule, | ||||
|         ReactiveFormsModule, | ||||
|         NgbModalModule, | ||||
|       ], | ||||
|     }) | ||||
|  | ||||
|     consumptionTemplateService = TestBed.inject(ConsumptionTemplateService) | ||||
|     jest.spyOn(consumptionTemplateService, 'listAll').mockReturnValue( | ||||
|       of({ | ||||
|         count: templates.length, | ||||
|         all: templates.map((o) => o.id), | ||||
|         results: templates, | ||||
|       }) | ||||
|     ) | ||||
|     modalService = TestBed.inject(NgbModal) | ||||
|     toastService = TestBed.inject(ToastService) | ||||
|  | ||||
|     fixture = TestBed.createComponent(ConsumptionTemplatesListComponent) | ||||
|     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 ConsumptionTemplateEditDialogComponent | ||||
|  | ||||
|     // fail first | ||||
|     editDialog.failed.emit({ error: 'error creating item' }) | ||||
|     expect(toastErrorSpy).toHaveBeenCalled() | ||||
|     expect(reloadSpy).not.toHaveBeenCalled() | ||||
|  | ||||
|     // succeed | ||||
|     editDialog.succeeded.emit(templates[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 ConsumptionTemplateEditDialogComponent | ||||
|     expect(editDialog.object).toEqual(templates[0]) | ||||
|  | ||||
|     // fail first | ||||
|     editDialog.failed.emit({ error: 'error editing item' }) | ||||
|     expect(toastErrorSpy).toHaveBeenCalled() | ||||
|     expect(reloadSpy).not.toHaveBeenCalled() | ||||
|  | ||||
|     // succeed | ||||
|     editDialog.succeeded.emit(templates[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(consumptionTemplateService, '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() | ||||
|   }) | ||||
| }) | ||||
| @@ -0,0 +1,109 @@ | ||||
| import { Component, OnInit } from '@angular/core' | ||||
| import { ConsumptionTemplateService } from 'src/app/services/rest/consumption-template.service' | ||||
| import { ComponentWithPermissions } from '../../with-permissions/with-permissions.component' | ||||
| import { Subject, takeUntil } from 'rxjs' | ||||
| import { PaperlessConsumptionTemplate } from 'src/app/data/paperless-consumption-template' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { PermissionsService } from 'src/app/services/permissions.service' | ||||
| import { | ||||
|   ConsumptionTemplateEditDialogComponent, | ||||
|   DOCUMENT_SOURCE_OPTIONS, | ||||
| } from '../../common/edit-dialog/consumption-template-edit-dialog/consumption-template-edit-dialog.component' | ||||
| import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | ||||
| import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'pngx-consumption-templates-list', | ||||
|   templateUrl: './consumption-templates-list.component.html', | ||||
|   styleUrls: ['./consumption-templates-list.component.scss'], | ||||
| }) | ||||
| export class ConsumptionTemplatesListComponent | ||||
|   extends ComponentWithPermissions | ||||
|   implements OnInit | ||||
| { | ||||
|   public templates: PaperlessConsumptionTemplate[] = [] | ||||
|  | ||||
|   private unsubscribeNotifier: Subject<any> = new Subject() | ||||
|  | ||||
|   constructor( | ||||
|     private consumptionTemplateService: ConsumptionTemplateService, | ||||
|     public permissionsService: PermissionsService, | ||||
|     private modalService: NgbModal, | ||||
|     private toastService: ToastService | ||||
|   ) { | ||||
|     super() | ||||
|   } | ||||
|  | ||||
|   ngOnInit() { | ||||
|     this.reload() | ||||
|   } | ||||
|  | ||||
|   reload() { | ||||
|     this.consumptionTemplateService | ||||
|       .listAll() | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((r) => { | ||||
|         this.templates = r.results | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   getSourceList(template: PaperlessConsumptionTemplate): string { | ||||
|     return template.sources | ||||
|       .map((id) => DOCUMENT_SOURCE_OPTIONS.find((s) => s.id === id).name) | ||||
|       .join(', ') | ||||
|   } | ||||
|  | ||||
|   editTemplate(rule: PaperlessConsumptionTemplate) { | ||||
|     const modal = this.modalService.open( | ||||
|       ConsumptionTemplateEditDialogComponent, | ||||
|       { | ||||
|         backdrop: 'static', | ||||
|         size: 'xl', | ||||
|       } | ||||
|     ) | ||||
|     modal.componentInstance.dialogMode = rule | ||||
|       ? EditDialogMode.EDIT | ||||
|       : EditDialogMode.CREATE | ||||
|     modal.componentInstance.object = rule | ||||
|     modal.componentInstance.succeeded | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((newTemplate) => { | ||||
|         this.toastService.showInfo( | ||||
|           $localize`Saved template "${newTemplate.name}".` | ||||
|         ) | ||||
|         this.consumptionTemplateService.clearCache() | ||||
|         this.reload() | ||||
|       }) | ||||
|     modal.componentInstance.failed | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe((e) => { | ||||
|         this.toastService.showError($localize`Error saving template.`, e) | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   deleteTemplate(rule: PaperlessConsumptionTemplate) { | ||||
|     const modal = this.modalService.open(ConfirmDialogComponent, { | ||||
|       backdrop: 'static', | ||||
|     }) | ||||
|     modal.componentInstance.title = $localize`Confirm delete template` | ||||
|     modal.componentInstance.messageBold = $localize`This operation will permanently delete this template.` | ||||
|     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.consumptionTemplateService.delete(rule).subscribe({ | ||||
|         next: () => { | ||||
|           modal.close() | ||||
|           this.toastService.showInfo($localize`Deleted template`) | ||||
|           this.consumptionTemplateService.clearCache() | ||||
|           this.reload() | ||||
|         }, | ||||
|         error: (e) => { | ||||
|           this.toastService.showError($localize`Error deleting template.`, e) | ||||
|         }, | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										41
									
								
								src-ui/src/app/data/paperless-consumption-template.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src-ui/src/app/data/paperless-consumption-template.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| import { ObjectWithId } from './object-with-id' | ||||
|  | ||||
| export enum DocumentSource { | ||||
|   ConsumeFolder = 1, | ||||
|   ApiUpload = 2, | ||||
|   MailFetch = 3, | ||||
| } | ||||
|  | ||||
| export interface PaperlessConsumptionTemplate extends ObjectWithId { | ||||
|   name: string | ||||
|  | ||||
|   order: number | ||||
|  | ||||
|   sources: DocumentSource[] | ||||
|  | ||||
|   filter_filename: string | ||||
|  | ||||
|   filter_path?: string | ||||
|  | ||||
|   filter_mailrule?: number // PaperlessMailRule.id | ||||
|  | ||||
|   assign_title?: string | ||||
|  | ||||
|   assign_tags?: number[] // PaperlessTag.id | ||||
|  | ||||
|   assign_document_type?: number // PaperlessDocumentType.id | ||||
|  | ||||
|   assign_correspondent?: number // PaperlessCorrespondent.id | ||||
|  | ||||
|   assign_storage_path?: number // PaperlessStoragePath.id | ||||
|  | ||||
|   assign_owner?: number // PaperlessUser.id | ||||
|  | ||||
|   assign_view_users?: number[] // [PaperlessUser.id] | ||||
|  | ||||
|   assign_view_groups?: number[] // [PaperlessGroup.id] | ||||
|  | ||||
|   assign_change_users?: number[] // [PaperlessUser.id] | ||||
|  | ||||
|   assign_change_groups?: number[] // [PaperlessGroup.id] | ||||
| } | ||||
| @@ -22,6 +22,7 @@ export enum MailAction { | ||||
| export enum MailMetadataTitleOption { | ||||
|   FromSubject = 1, | ||||
|   FromFilename = 2, | ||||
|   None = 3, | ||||
| } | ||||
|  | ||||
| export enum MailMetadataCorrespondentOption { | ||||
| @@ -67,4 +68,6 @@ export interface PaperlessMailRule extends ObjectWithPermissions { | ||||
|   assign_correspondent_from?: MailMetadataCorrespondentOption | ||||
|  | ||||
|   assign_correspondent?: number // PaperlessCorrespondent.id | ||||
|  | ||||
|   assign_owner_from_rule: boolean | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import { PaperlessGroup } from 'src/app/data/paperless-group' | ||||
| import { ObjectWithId } from './object-with-id' | ||||
|  | ||||
| export interface PaperlessUser extends ObjectWithId { | ||||
|   | ||||
| @@ -252,6 +252,10 @@ describe('PermissionsService', () => { | ||||
|         'view_sharelink', | ||||
|         'change_sharelink', | ||||
|         'delete_sharelink', | ||||
|         'add_consumptiontemplate', | ||||
|         'view_consumptiontemplate', | ||||
|         'change_consumptiontemplate', | ||||
|         'delete_consumptiontemplate', | ||||
|       ], | ||||
|       { | ||||
|         username: 'testuser', | ||||
|   | ||||
| @@ -25,6 +25,7 @@ export enum PermissionType { | ||||
|   Group = '%s_group', | ||||
|   Admin = '%s_logentry', | ||||
|   ShareLink = '%s_sharelink', | ||||
|   ConsumptionTemplate = '%s_consumptiontemplate', | ||||
| } | ||||
|  | ||||
| @Injectable({ | ||||
|   | ||||
| @@ -0,0 +1,64 @@ | ||||
| import { HttpTestingController } from '@angular/common/http/testing' | ||||
| import { TestBed } from '@angular/core/testing' | ||||
| import { Subscription } from 'rxjs' | ||||
| import { environment } from 'src/environments/environment' | ||||
| import { commonAbstractPaperlessServiceTests } from './abstract-paperless-service.spec' | ||||
| import { ConsumptionTemplateService } from './consumption-template.service' | ||||
| import { | ||||
|   DocumentSource, | ||||
|   PaperlessConsumptionTemplate, | ||||
| } from 'src/app/data/paperless-consumption-template' | ||||
|  | ||||
| let httpTestingController: HttpTestingController | ||||
| let service: ConsumptionTemplateService | ||||
| const endpoint = 'consumption_templates' | ||||
| const templates: PaperlessConsumptionTemplate[] = [ | ||||
|   { | ||||
|     name: 'Template 1', | ||||
|     id: 1, | ||||
|     order: 1, | ||||
|     filter_filename: '*test*', | ||||
|     filter_path: null, | ||||
|     sources: [DocumentSource.ApiUpload], | ||||
|     assign_correspondent: 2, | ||||
|   }, | ||||
|   { | ||||
|     name: 'Template 2', | ||||
|     id: 2, | ||||
|     order: 2, | ||||
|     filter_filename: null, | ||||
|     filter_path: '/test/', | ||||
|     sources: [DocumentSource.ConsumeFolder, DocumentSource.ApiUpload], | ||||
|     assign_document_type: 1, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| // run common tests | ||||
| commonAbstractPaperlessServiceTests( | ||||
|   'consumption_templates', | ||||
|   ConsumptionTemplateService | ||||
| ) | ||||
|  | ||||
| describe(`Additional service tests for ConsumptionTemplateService`, () => { | ||||
|   it('should reload', () => { | ||||
|     service.reload() | ||||
|     const req = httpTestingController.expectOne( | ||||
|       `${environment.apiBaseUrl}${endpoint}/?page=1&page_size=100000` | ||||
|     ) | ||||
|     req.flush({ | ||||
|       results: templates, | ||||
|     }) | ||||
|     expect(service.allTemplates).toEqual(templates) | ||||
|   }) | ||||
|  | ||||
|   beforeEach(() => { | ||||
|     // Dont need to setup again | ||||
|  | ||||
|     httpTestingController = TestBed.inject(HttpTestingController) | ||||
|     service = TestBed.inject(ConsumptionTemplateService) | ||||
|   }) | ||||
|  | ||||
|   afterEach(() => { | ||||
|     httpTestingController.verify() | ||||
|   }) | ||||
| }) | ||||
							
								
								
									
										42
									
								
								src-ui/src/app/services/rest/consumption-template.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src-ui/src/app/services/rest/consumption-template.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import { HttpClient } from '@angular/common/http' | ||||
| import { Injectable } from '@angular/core' | ||||
| import { tap } from 'rxjs' | ||||
| import { PaperlessConsumptionTemplate } from 'src/app/data/paperless-consumption-template' | ||||
| import { AbstractPaperlessService } from './abstract-paperless-service' | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class ConsumptionTemplateService extends AbstractPaperlessService<PaperlessConsumptionTemplate> { | ||||
|   loading: boolean | ||||
|  | ||||
|   constructor(http: HttpClient) { | ||||
|     super(http, 'consumption_templates') | ||||
|   } | ||||
|  | ||||
|   public reload() { | ||||
|     this.loading = true | ||||
|     this.listAll().subscribe((r) => { | ||||
|       this.templates = r.results | ||||
|       this.loading = false | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   private templates: PaperlessConsumptionTemplate[] = [] | ||||
|  | ||||
|   public get allTemplates(): PaperlessConsumptionTemplate[] { | ||||
|     return this.templates | ||||
|   } | ||||
|  | ||||
|   create(o: PaperlessConsumptionTemplate) { | ||||
|     return super.create(o).pipe(tap(() => this.reload())) | ||||
|   } | ||||
|  | ||||
|   update(o: PaperlessConsumptionTemplate) { | ||||
|     return super.update(o).pipe(tap(() => this.reload())) | ||||
|   } | ||||
|  | ||||
|   delete(o: PaperlessConsumptionTemplate) { | ||||
|     return super.delete(o).pipe(tap(() => this.reload())) | ||||
|   } | ||||
| } | ||||
| @@ -28,6 +28,7 @@ const mail_rules = [ | ||||
|     attachment_type: MailFilterAttachmentType.Everything, | ||||
|     action: MailAction.MarkRead, | ||||
|     assign_title_from: MailMetadataTitleOption.FromSubject, | ||||
|     assign_owner_from_rule: true, | ||||
|   }, | ||||
|   { | ||||
|     name: 'Mail Rule 2', | ||||
| @@ -44,6 +45,7 @@ const mail_rules = [ | ||||
|     attachment_type: MailFilterAttachmentType.Everything, | ||||
|     action: MailAction.Delete, | ||||
|     assign_title_from: MailMetadataTitleOption.FromSubject, | ||||
|     assign_owner_from_rule: true, | ||||
|   }, | ||||
|   { | ||||
|     name: 'Mail Rule 3', | ||||
| @@ -60,6 +62,7 @@ const mail_rules = [ | ||||
|     attachment_type: MailFilterAttachmentType.Everything, | ||||
|     action: MailAction.Flag, | ||||
|     assign_title_from: MailMetadataTitleOption.FromSubject, | ||||
|     assign_owner_from_rule: false, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
|   | ||||
| @@ -170,7 +170,8 @@ a, a:hover, | ||||
| } | ||||
|  | ||||
| .btn-link:hover, | ||||
| .btn-link:active { | ||||
| .btn-link:active, | ||||
| .btn-link:focus-visible { | ||||
|   color: var(--pngx-primary-darken-15) !important; | ||||
| } | ||||
|  | ||||
| @@ -456,7 +457,7 @@ ul.pagination { | ||||
| } | ||||
|  | ||||
| table.table { | ||||
|   color: var(--bs-body-color); | ||||
|   --bs-table-color: var(--bs-body-color); | ||||
|   --bs-table-bg: var(--bs-light-rgb); | ||||
|  | ||||
|   .des,.asc { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon