mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-30 03:56:23 -05:00 
			
		
		
		
	Merge pull request #2000 from paperless-ngx/feature-frontend-paperless-mail
Feature: frontend paperless mail
This commit is contained in:
		| @@ -35,6 +35,16 @@ describe('settings', () => { | ||||
|             req.reply(response) | ||||
|           } | ||||
|         ).as('savedViews') | ||||
|  | ||||
|         cy.intercept('http://localhost:8000/api/mail_accounts/*', { | ||||
|           fixture: 'mail_accounts/mail_accounts.json', | ||||
|         }) | ||||
|         cy.intercept('http://localhost:8000/api/mail_rules/*', { | ||||
|           fixture: 'mail_rules/mail_rules.json', | ||||
|         }).as('mailRules') | ||||
|         cy.intercept('http://localhost:8000/api/tasks/', { | ||||
|           fixture: 'tasks/tasks.json', | ||||
|         }) | ||||
|       }) | ||||
|  | ||||
|       cy.fixture('documents/documents.json').then((documentsJson) => { | ||||
| @@ -48,7 +58,6 @@ describe('settings', () => { | ||||
|  | ||||
|     cy.viewport(1024, 1600) | ||||
|     cy.visit('/settings') | ||||
|     cy.wait('@savedViews') | ||||
|   }) | ||||
|  | ||||
|   it('should activate / deactivate save button when settings change and are saved', () => { | ||||
| @@ -64,7 +73,7 @@ describe('settings', () => { | ||||
|     cy.contains('a', 'Dashboard').click() | ||||
|     cy.contains('You have unsaved changes') | ||||
|     cy.contains('button', 'Cancel').click() | ||||
|     cy.contains('button', 'Save').click().wait('@savedViews') | ||||
|     cy.contains('button', 'Save').click().wait('@savedViews').wait(2000) | ||||
|     cy.contains('a', 'Dashboard').click() | ||||
|     cy.contains('You have unsaved changes').should('not.exist') | ||||
|   }) | ||||
| @@ -77,16 +86,16 @@ describe('settings', () => { | ||||
|   }) | ||||
|  | ||||
|   it('should remove saved view from sidebar when unset', () => { | ||||
|     cy.contains('a', 'Saved views').click() | ||||
|     cy.contains('a', 'Saved views').click().wait(2000) | ||||
|     cy.get('#show_in_sidebar_1').click() | ||||
|     cy.contains('button', 'Save').click().wait('@savedViews') | ||||
|     cy.contains('button', 'Save').click().wait('@savedViews').wait(2000) | ||||
|     cy.contains('li', 'Inbox').should('not.exist') | ||||
|   }) | ||||
|  | ||||
|   it('should remove saved view from dashboard when unset', () => { | ||||
|     cy.contains('a', 'Saved views').click() | ||||
|     cy.get('#show_on_dashboard_1').click() | ||||
|     cy.contains('button', 'Save').click().wait('@savedViews') | ||||
|     cy.contains('button', 'Save').click().wait('@savedViews').wait(2000) | ||||
|     cy.visit('/dashboard') | ||||
|     cy.get('app-saved-view-widget').contains('Inbox').should('not.exist') | ||||
|   }) | ||||
|   | ||||
							
								
								
									
										27
									
								
								src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src-ui/cypress/fixtures/mail_accounts/mail_accounts.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| { | ||||
|     "count": 2, | ||||
|     "next": null, | ||||
|     "previous": null, | ||||
|     "results": [ | ||||
|         { | ||||
|             "id": 1, | ||||
|             "name": "IMAP Server", | ||||
|             "imap_server": "imap.example.com", | ||||
|             "imap_port": 993, | ||||
|             "imap_security": 2, | ||||
|             "username": "inbox@example.com", | ||||
|             "password": "pass", | ||||
|             "character_set": "UTF-8" | ||||
|         }, | ||||
|         { | ||||
|             "id": 2, | ||||
|             "name": "Gmail", | ||||
|             "imap_server": "imap.gmail.com", | ||||
|             "imap_port": 993, | ||||
|             "imap_security": 2, | ||||
|             "username": "user@gmail.com", | ||||
|             "password": "pass", | ||||
|             "character_set": "UTF-8" | ||||
|         } | ||||
|     ] | ||||
| } | ||||
							
								
								
									
										29
									
								
								src-ui/cypress/fixtures/mail_rules/mail_rules.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								src-ui/cypress/fixtures/mail_rules/mail_rules.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | ||||
| { | ||||
|     "count": 1, | ||||
|     "next": null, | ||||
|     "previous": null, | ||||
|     "results": [ | ||||
|         { | ||||
|             "id": 1, | ||||
|             "name": "Gmail", | ||||
|             "account": 2, | ||||
|             "folder": "INBOX", | ||||
|             "filter_from": null, | ||||
|             "filter_subject": "[paperless]", | ||||
|             "filter_body": null, | ||||
|             "filter_attachment_filename": null, | ||||
|             "maximum_age": 30, | ||||
|             "action": 3, | ||||
|             "action_parameter": null, | ||||
|             "assign_title_from": 1, | ||||
|             "assign_tags": [ | ||||
|                 9 | ||||
|             ], | ||||
|             "assign_correspondent_from": 1, | ||||
|             "assign_correspondent": 2, | ||||
|             "assign_document_type": null, | ||||
|             "order": 0, | ||||
|             "attachment_type": 2 | ||||
|         } | ||||
|     ] | ||||
| } | ||||
| @@ -1 +1,44 @@ | ||||
| {"count":3,"next":null,"previous":null,"results":[{"id":1,"name":"Inbox","show_on_dashboard":true,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"18"}]},{"id":2,"name":"Recently Added","show_on_dashboard":true,"show_in_sidebar":false,"sort_field":"created","sort_reverse":true,"filter_rules":[]},{"id":11,"name":"Taxes","show_on_dashboard":false,"show_in_sidebar":true,"sort_field":"created","sort_reverse":true,"filter_rules":[{"rule_type":6,"value":"39"}]}]} | ||||
| { | ||||
|     "count": 3, | ||||
|     "next": null, | ||||
|     "previous": null, | ||||
|     "results": [ | ||||
|         { | ||||
|             "id": 1, | ||||
|             "name": "Inbox", | ||||
|             "show_on_dashboard": true, | ||||
|             "show_in_sidebar": true, | ||||
|             "sort_field": "created", | ||||
|             "sort_reverse": true, | ||||
|             "filter_rules": [ | ||||
|                 { | ||||
|                     "rule_type": 6, | ||||
|                     "value": "18" | ||||
|                 } | ||||
|             ] | ||||
|         }, | ||||
|         { | ||||
|             "id": 2, | ||||
|             "name": "Recently Added", | ||||
|             "show_on_dashboard": true, | ||||
|             "show_in_sidebar": false, | ||||
|             "sort_field": "created", | ||||
|             "sort_reverse": true, | ||||
|             "filter_rules": [] | ||||
|         }, | ||||
|         { | ||||
|             "id": 11, | ||||
|             "name": "Taxes", | ||||
|             "show_on_dashboard": false, | ||||
|             "show_in_sidebar": true, | ||||
|             "sort_field": "created", | ||||
|             "sort_reverse": true, | ||||
|             "filter_rules": [ | ||||
|                 { | ||||
|                     "rule_type": 6, | ||||
|                     "value": "39" | ||||
|                 } | ||||
|             ] | ||||
|         } | ||||
|     ] | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -47,6 +47,11 @@ const routes: Routes = [ | ||||
|         component: SettingsComponent, | ||||
|         canDeactivate: [DirtyFormGuard], | ||||
|       }, | ||||
|       { | ||||
|         path: 'settings/:section', | ||||
|         component: SettingsComponent, | ||||
|         canDeactivate: [DirtyFormGuard], | ||||
|       }, | ||||
|       { path: 'tasks', component: TasksComponent }, | ||||
|     ], | ||||
|   }, | ||||
|   | ||||
| @@ -191,21 +191,13 @@ export class AppComponent implements OnInit, OnDestroy { | ||||
|       }, | ||||
|       { | ||||
|         anchorId: 'tour.settings', | ||||
|         content: $localize`Check out the settings for various tweaks to the web app or to toggle settings for saved views.`, | ||||
|         content: $localize`Check out the settings for various tweaks to the web app, toggle settings for saved views or setup e-mail checking.`, | ||||
|         route: '/settings', | ||||
|         enableBackdrop: true, | ||||
|         prevBtnTitle, | ||||
|         nextBtnTitle, | ||||
|         endBtnTitle, | ||||
|       }, | ||||
|       { | ||||
|         anchorId: 'tour.admin', | ||||
|         content: $localize`The Admin area contains more advanced controls as well as the settings for automatic e-mail fetching.`, | ||||
|         enableBackdrop: true, | ||||
|         prevBtnTitle, | ||||
|         nextBtnTitle, | ||||
|         endBtnTitle, | ||||
|       }, | ||||
|       { | ||||
|         anchorId: 'tour.outro', | ||||
|         title: $localize`Thank you! 🙏`, | ||||
|   | ||||
| @@ -39,6 +39,7 @@ import { NgxFileDropModule } from 'ngx-file-drop' | ||||
| import { TextComponent } from './components/common/input/text/text.component' | ||||
| import { SelectComponent } from './components/common/input/select/select.component' | ||||
| import { CheckComponent } from './components/common/input/check/check.component' | ||||
| import { PasswordComponent } from './components/common/input/password/password.component' | ||||
| import { SaveViewConfigDialogComponent } from './components/document-list/save-view-config-dialog/save-view-config-dialog.component' | ||||
| import { TagsComponent } from './components/common/input/tags/tags.component' | ||||
| import { SortableDirective } from './directives/sortable.directive' | ||||
| @@ -76,6 +77,8 @@ import { StoragePathEditDialogComponent } from './components/common/edit-dialog/ | ||||
| import { SettingsService } from './services/settings.service' | ||||
| import { TasksComponent } from './components/manage/tasks/tasks.component' | ||||
| import { TourNgBootstrapModule } from 'ngx-ui-tour-ng-bootstrap' | ||||
| import { MailAccountEditDialogComponent } from './components/common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component' | ||||
| import { MailRuleEditDialogComponent } from './components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component' | ||||
|  | ||||
| import localeBe from '@angular/common/locales/be' | ||||
| import localeCs from '@angular/common/locales/cs' | ||||
| @@ -157,6 +160,7 @@ function initializeApp(settings: SettingsService) { | ||||
|     TextComponent, | ||||
|     SelectComponent, | ||||
|     CheckComponent, | ||||
|     PasswordComponent, | ||||
|     SaveViewConfigDialogComponent, | ||||
|     TagsComponent, | ||||
|     SortableDirective, | ||||
| @@ -180,6 +184,8 @@ function initializeApp(settings: SettingsService) { | ||||
|     DocumentAsnComponent, | ||||
|     DocumentCommentsComponent, | ||||
|     TasksComponent, | ||||
|     MailAccountEditDialogComponent, | ||||
|     MailRuleEditDialogComponent, | ||||
|   ], | ||||
|   imports: [ | ||||
|     BrowserModule, | ||||
|   | ||||
| @@ -174,13 +174,6 @@ | ||||
|               </svg><span> <ng-container i18n>Settings</ng-container></span> | ||||
|             </a> | ||||
|           </li> | ||||
|           <li class="nav-item" tourAnchor="tour.admin"> | ||||
|             <a class="nav-link" href="admin/" ngbPopover="Admin" 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#toggles"/> | ||||
|               </svg><span> <ng-container i18n>Admin</ng-container></span> | ||||
|             </a> | ||||
|           </li> | ||||
|         </ul> | ||||
|  | ||||
|         <h6 class="sidebar-heading px-3 mt-auto pt-4 mb-1 text-muted"> | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit- | ||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-correspondent-edit-dialog', | ||||
| @@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service' | ||||
|   styleUrls: ['./correspondent-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class CorrespondentEditDialogComponent extends EditDialogComponent<PaperlessCorrespondent> { | ||||
|   constructor( | ||||
|     service: CorrespondentService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     toastService: ToastService | ||||
|   ) { | ||||
|     super(service, activeModal, toastService) | ||||
|   constructor(service: CorrespondentService, activeModal: NgbActiveModal) { | ||||
|     super(service, activeModal) | ||||
|   } | ||||
|  | ||||
|   getCreateTitle() { | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit- | ||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-document-type-edit-dialog', | ||||
| @@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service' | ||||
|   styleUrls: ['./document-type-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class DocumentTypeEditDialogComponent extends EditDialogComponent<PaperlessDocumentType> { | ||||
|   constructor( | ||||
|     service: DocumentTypeService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     toastService: ToastService | ||||
|   ) { | ||||
|     super(service, activeModal, toastService) | ||||
|   constructor(service: DocumentTypeService, activeModal: NgbActiveModal) { | ||||
|     super(service, activeModal) | ||||
|   } | ||||
|  | ||||
|   getCreateTitle() { | ||||
|   | ||||
| @@ -2,11 +2,9 @@ import { Directive, EventEmitter, Input, OnInit, Output } from '@angular/core' | ||||
| import { FormGroup } from '@angular/forms' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { Observable } from 'rxjs' | ||||
| import { map } from 'rxjs/operators' | ||||
| import { MATCHING_ALGORITHMS, MATCH_AUTO } from 'src/app/data/matching-model' | ||||
| import { ObjectWithId } from 'src/app/data/object-with-id' | ||||
| import { AbstractPaperlessService } from 'src/app/services/rest/abstract-paperless-service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
|  | ||||
| @Directive() | ||||
| export abstract class EditDialogComponent<T extends ObjectWithId> | ||||
| @@ -14,8 +12,7 @@ export abstract class EditDialogComponent<T extends ObjectWithId> | ||||
| { | ||||
|   constructor( | ||||
|     private service: AbstractPaperlessService<T>, | ||||
|     private activeModal: NgbActiveModal, | ||||
|     private toastService: ToastService | ||||
|     private activeModal: NgbActiveModal | ||||
|   ) {} | ||||
|  | ||||
|   @Input() | ||||
| @@ -95,16 +92,16 @@ export abstract class EditDialogComponent<T extends ObjectWithId> | ||||
|         break | ||||
|     } | ||||
|     this.networkActive = true | ||||
|     serverResponse.subscribe( | ||||
|       (result) => { | ||||
|     serverResponse.subscribe({ | ||||
|       next: (result) => { | ||||
|         this.activeModal.close() | ||||
|         this.success.emit(result) | ||||
|       }, | ||||
|       (error) => { | ||||
|       error: (error) => { | ||||
|         this.error = error.error | ||||
|         this.networkActive = false | ||||
|       } | ||||
|     ) | ||||
|       }, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   cancel() { | ||||
|   | ||||
| @@ -0,0 +1,26 @@ | ||||
| <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"> | ||||
|         <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> | ||||
|         <app-input-text i18n-title title="IMAP Server" formControlName="imap_server" [error]="error?.imap_server"></app-input-text> | ||||
|         <app-input-text i18n-title title="IMAP Port" formControlName="imap_port" [error]="error?.imap_port"></app-input-text> | ||||
|         <app-input-select i18n-title title="IMAP Security" [items]="imapSecurityOptions" formControlName="imap_security"></app-input-select> | ||||
|       </div> | ||||
|       <div class="col"> | ||||
|         <app-input-text i18n-title title="Username" formControlName="username" [error]="error?.username"></app-input-text> | ||||
|         <app-input-password i18n-title title="Password" formControlName="password" [error]="error?.password"></app-input-password> | ||||
|         <app-input-text i18n-title title="Character Set" formControlName="character_set" [error]="error?.character_set"></app-input-text> | ||||
|       </div> | ||||
|     </div> | ||||
|   </div> | ||||
|   <div class="modal-footer"> | ||||
|     <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> | ||||
|     <button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button> | ||||
|   </div> | ||||
| </form> | ||||
| @@ -0,0 +1,50 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { FormControl, FormGroup } from '@angular/forms' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||
| import { | ||||
|   IMAPSecurity, | ||||
|   PaperlessMailAccount, | ||||
| } from 'src/app/data/paperless-mail-account' | ||||
| import { MailAccountService } from 'src/app/services/rest/mail-account.service' | ||||
|  | ||||
| const IMAP_SECURITY_OPTIONS = [ | ||||
|   { id: IMAPSecurity.None, name: $localize`No encryption` }, | ||||
|   { id: IMAPSecurity.SSL, name: $localize`SSL` }, | ||||
|   { id: IMAPSecurity.STARTTLS, name: $localize`STARTTLS` }, | ||||
| ] | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-mail-account-edit-dialog', | ||||
|   templateUrl: './mail-account-edit-dialog.component.html', | ||||
|   styleUrls: ['./mail-account-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class MailAccountEditDialogComponent extends EditDialogComponent<PaperlessMailAccount> { | ||||
|   constructor(service: MailAccountService, activeModal: NgbActiveModal) { | ||||
|     super(service, activeModal) | ||||
|   } | ||||
|  | ||||
|   getCreateTitle() { | ||||
|     return $localize`Create new mail account` | ||||
|   } | ||||
|  | ||||
|   getEditTitle() { | ||||
|     return $localize`Edit mail account` | ||||
|   } | ||||
|  | ||||
|   getForm(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(null), | ||||
|       imap_server: new FormControl(null), | ||||
|       imap_port: new FormControl(null), | ||||
|       imap_security: new FormControl(IMAPSecurity.SSL), | ||||
|       username: new FormControl(null), | ||||
|       password: new FormControl(null), | ||||
|       character_set: new FormControl('UTF-8'), | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   get imapSecurityOptions() { | ||||
|     return IMAP_SECURITY_OPTIONS | ||||
|   } | ||||
| } | ||||
| @@ -0,0 +1,39 @@ | ||||
| <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"> | ||||
|         <app-input-text i18n-title title="Name" formControlName="name" [error]="error?.name"></app-input-text> | ||||
|         <app-input-select i18n-title title="Account" [items]="accounts" formControlName="account"></app-input-select> | ||||
|         <app-input-text i18n-title title="Folder" formControlName="folder" i18n-hint hint="Subfolders must be separated by a delimiter, often a dot ('.') or slash ('/'), but it varies by mail server." [error]="error?.folder"></app-input-text> | ||||
|         <app-input-number i18n-title title="Maximum age (days)" formControlName="maximum_age" [showAdd]="false" [error]="error?.maximum_age"></app-input-number> | ||||
|         <app-input-select i18n-title title="Attachment type" [items]="attachmentTypeOptions" formControlName="attachment_type"></app-input-select> | ||||
|       </div> | ||||
|       <div class="col"> | ||||
|         <p class="small" i18n>Paperless will only process mails that match <em>all</em> of the filters specified below.</p> | ||||
|         <app-input-text i18n-title title="Filter from" formControlName="filter_from" [error]="error?.filter_from"></app-input-text> | ||||
|         <app-input-text i18n-title title="Filter subject" formControlName="filter_subject" [error]="error?.filter_subject"></app-input-text> | ||||
|         <app-input-text i18n-title title="Filter body" formControlName="filter_body" [error]="error?.filter_body"></app-input-text> | ||||
|         <app-input-text i18n-title title="Filter attachment filename" formControlName="filter_attachment_filename" i18n-hint hint="Only consume documents which entirely match this filename if specified. Wildcards such as *.pdf or *invoice* are allowed. Case insensitive." [error]="error?.filter_attachment_filename"></app-input-text> | ||||
|       </div> | ||||
|       <div class="col"> | ||||
|         <app-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."></app-input-select> | ||||
|         <app-input-text i18n-title title="Action parameter" *ngIf="showActionParamField" formControlName="action_parameter" [error]="error?.action_parameter"></app-input-text> | ||||
|         <app-input-select i18n-title title="Assign title from" [items]="metadataTitleOptions" formControlName="assign_title_from"></app-input-select> | ||||
|         <app-input-tags [allowCreate]="false" formControlName="assign_tags"></app-input-tags> | ||||
|         <app-input-select i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></app-input-select> | ||||
|         <app-input-select i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></app-input-select> | ||||
|         <app-input-select *ngIf="showCorrespondentField" i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></app-input-select> | ||||
|       </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,180 @@ | ||||
| import { Component } from '@angular/core' | ||||
| import { FormControl, FormGroup } from '@angular/forms' | ||||
| import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { first } from 'rxjs' | ||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||
| import { PaperlessCorrespondent } from 'src/app/data/paperless-correspondent' | ||||
| import { PaperlessDocumentType } from 'src/app/data/paperless-document-type' | ||||
| import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account' | ||||
| import { | ||||
|   MailAction, | ||||
|   MailFilterAttachmentType, | ||||
|   MailMetadataCorrespondentOption, | ||||
|   MailMetadataTitleOption, | ||||
|   PaperlessMailRule, | ||||
| } from 'src/app/data/paperless-mail-rule' | ||||
| import { CorrespondentService } from 'src/app/services/rest/correspondent.service' | ||||
| import { DocumentTypeService } from 'src/app/services/rest/document-type.service' | ||||
| import { MailAccountService } from 'src/app/services/rest/mail-account.service' | ||||
| import { MailRuleService } from 'src/app/services/rest/mail-rule.service' | ||||
|  | ||||
| const ATTACHMENT_TYPE_OPTIONS = [ | ||||
|   { | ||||
|     id: MailFilterAttachmentType.Attachments, | ||||
|     name: $localize`Only process attachments.`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailFilterAttachmentType.Everything, | ||||
|     name: $localize`Process all files, including 'inline' attachments.`, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const ACTION_OPTIONS = [ | ||||
|   { | ||||
|     id: MailAction.Delete, | ||||
|     name: $localize`Delete`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailAction.Move, | ||||
|     name: $localize`Move to specified folder`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailAction.MarkRead, | ||||
|     name: $localize`Mark as read, don't process read mails`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailAction.Flag, | ||||
|     name: $localize`Flag the mail, don't process flagged mails`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailAction.Tag, | ||||
|     name: $localize`Tag the mail with specified tag, don't process tagged mails`, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const METADATA_TITLE_OPTIONS = [ | ||||
|   { | ||||
|     id: MailMetadataTitleOption.FromSubject, | ||||
|     name: $localize`Use subject as title`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailMetadataTitleOption.FromFilename, | ||||
|     name: $localize`Use attachment filename as title`, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| const METADATA_CORRESPONDENT_OPTIONS = [ | ||||
|   { | ||||
|     id: MailMetadataCorrespondentOption.FromNothing, | ||||
|     name: $localize`Do not assign a correspondent`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailMetadataCorrespondentOption.FromEmail, | ||||
|     name: $localize`Use mail address`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailMetadataCorrespondentOption.FromName, | ||||
|     name: $localize`Use name (or mail address if not available)`, | ||||
|   }, | ||||
|   { | ||||
|     id: MailMetadataCorrespondentOption.FromCustom, | ||||
|     name: $localize`Use correspondent selected below`, | ||||
|   }, | ||||
| ] | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-mail-rule-edit-dialog', | ||||
|   templateUrl: './mail-rule-edit-dialog.component.html', | ||||
|   styleUrls: ['./mail-rule-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class MailRuleEditDialogComponent extends EditDialogComponent<PaperlessMailRule> { | ||||
|   accounts: PaperlessMailAccount[] | ||||
|   correspondents: PaperlessCorrespondent[] | ||||
|   documentTypes: PaperlessDocumentType[] | ||||
|  | ||||
|   constructor( | ||||
|     service: MailRuleService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     accountService: MailAccountService, | ||||
|     correspondentService: CorrespondentService, | ||||
|     documentTypeService: DocumentTypeService | ||||
|   ) { | ||||
|     super(service, activeModal) | ||||
|  | ||||
|     accountService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.accounts = result.results)) | ||||
|  | ||||
|     correspondentService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.correspondents = result.results)) | ||||
|  | ||||
|     documentTypeService | ||||
|       .listAll() | ||||
|       .pipe(first()) | ||||
|       .subscribe((result) => (this.documentTypes = result.results)) | ||||
|   } | ||||
|  | ||||
|   getCreateTitle() { | ||||
|     return $localize`Create new mail rule` | ||||
|   } | ||||
|  | ||||
|   getEditTitle() { | ||||
|     return $localize`Edit mail rule` | ||||
|   } | ||||
|  | ||||
|   getForm(): FormGroup { | ||||
|     return new FormGroup({ | ||||
|       name: new FormControl(null), | ||||
|       account: new FormControl(null), | ||||
|       folder: new FormControl('INBOX'), | ||||
|       filter_from: new FormControl(null), | ||||
|       filter_subject: new FormControl(null), | ||||
|       filter_body: new FormControl(null), | ||||
|       filter_attachment_filename: new FormControl(null), | ||||
|       maximum_age: new FormControl(null), | ||||
|       attachment_type: new FormControl(MailFilterAttachmentType.Attachments), | ||||
|       action: new FormControl(MailAction.MarkRead), | ||||
|       action_parameter: new FormControl(null), | ||||
|       assign_title_from: new FormControl(MailMetadataTitleOption.FromSubject), | ||||
|       assign_tags: new FormControl([]), | ||||
|       assign_document_type: new FormControl(null), | ||||
|       assign_correspondent_from: new FormControl( | ||||
|         MailMetadataCorrespondentOption.FromNothing | ||||
|       ), | ||||
|       assign_correspondent: new FormControl(null), | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   get showCorrespondentField(): boolean { | ||||
|     return ( | ||||
|       this.objectForm?.get('assign_correspondent_from')?.value == | ||||
|       MailMetadataCorrespondentOption.FromCustom | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   get showActionParamField(): boolean { | ||||
|     return ( | ||||
|       this.objectForm?.get('action')?.value == MailAction.Move || | ||||
|       this.objectForm?.get('action')?.value == MailAction.Tag | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   get attachmentTypeOptions() { | ||||
|     return ATTACHMENT_TYPE_OPTIONS | ||||
|   } | ||||
|  | ||||
|   get actionOptions() { | ||||
|     return ACTION_OPTIONS | ||||
|   } | ||||
|  | ||||
|   get metadataTitleOptions() { | ||||
|     return METADATA_TITLE_OPTIONS | ||||
|   } | ||||
|  | ||||
|   get metadataCorrespondentOptions() { | ||||
|     return METADATA_CORRESPONDENT_OPTIONS | ||||
|   } | ||||
| } | ||||
| @@ -5,7 +5,6 @@ import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit- | ||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||
| import { PaperlessStoragePath } from 'src/app/data/paperless-storage-path' | ||||
| import { StoragePathService } from 'src/app/services/rest/storage-path.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-storage-path-edit-dialog', | ||||
| @@ -13,12 +12,8 @@ import { ToastService } from 'src/app/services/toast.service' | ||||
|   styleUrls: ['./storage-path-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class StoragePathEditDialogComponent extends EditDialogComponent<PaperlessStoragePath> { | ||||
|   constructor( | ||||
|     service: StoragePathService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     toastService: ToastService | ||||
|   ) { | ||||
|     super(service, activeModal, toastService) | ||||
|   constructor(service: StoragePathService, activeModal: NgbActiveModal) { | ||||
|     super(service, activeModal) | ||||
|   } | ||||
|  | ||||
|   get pathHint() { | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component' | ||||
| import { PaperlessTag } from 'src/app/data/paperless-tag' | ||||
| import { TagService } from 'src/app/services/rest/tag.service' | ||||
| import { ToastService } from 'src/app/services/toast.service' | ||||
| import { randomColor } from 'src/app/utils/color' | ||||
| import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||
|  | ||||
| @@ -14,12 +13,8 @@ import { DEFAULT_MATCHING_ALGORITHM } from 'src/app/data/matching-model' | ||||
|   styleUrls: ['./tag-edit-dialog.component.scss'], | ||||
| }) | ||||
| export class TagEditDialogComponent extends EditDialogComponent<PaperlessTag> { | ||||
|   constructor( | ||||
|     service: TagService, | ||||
|     activeModal: NgbActiveModal, | ||||
|     toastService: ToastService | ||||
|   ) { | ||||
|     super(service, activeModal, toastService) | ||||
|   constructor(service: TagService, activeModal: NgbActiveModal) { | ||||
|     super(service, activeModal) | ||||
|   } | ||||
|  | ||||
|   getCreateTitle() { | ||||
|   | ||||
| @@ -2,7 +2,7 @@ | ||||
|   <label class="form-label" [for]="inputId">{{title}}</label> | ||||
|   <div class="input-group" [class.is-invalid]="error"> | ||||
|     <input type="number" class="form-control" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)" [class.is-invalid]="error"> | ||||
|     <button class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button> | ||||
|     <button *ngIf="showAdd" class="btn btn-outline-secondary" type="button" id="button-addon1" (click)="nextAsn()" [disabled]="value">+1</button> | ||||
|   </div> | ||||
|   <div class="invalid-feedback"> | ||||
|     {{error}} | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| import { Component, forwardRef } from '@angular/core' | ||||
| import { Component, forwardRef, Input } from '@angular/core' | ||||
| import { NG_VALUE_ACCESSOR } from '@angular/forms' | ||||
| import { FILTER_ASN_ISNULL } from 'src/app/data/filter-rule-type' | ||||
| import { DocumentService } from 'src/app/services/rest/document.service' | ||||
| @@ -17,6 +17,9 @@ import { AbstractInputComponent } from '../abstract-input' | ||||
|   styleUrls: ['./number.component.scss'], | ||||
| }) | ||||
| export class NumberComponent extends AbstractInputComponent<number> { | ||||
|   @Input() | ||||
|   showAdd: boolean = true | ||||
|  | ||||
|   constructor(private documentService: DocumentService) { | ||||
|     super() | ||||
|   } | ||||
|   | ||||
| @@ -0,0 +1,8 @@ | ||||
| <div class="mb-3"> | ||||
|   <label class="form-label" [for]="inputId">{{title}}</label> | ||||
|   <input #inputField type="password" class="form-control" [class.is-invalid]="error" [id]="inputId" [(ngModel)]="value" (change)="onChange(value)"> | ||||
|   <small *ngIf="hint" class="form-text text-muted" [innerHTML]="hint | safeHtml"></small> | ||||
|   <div class="invalid-feedback"> | ||||
|     {{error}} | ||||
|   </div> | ||||
| </div> | ||||
| @@ -0,0 +1,21 @@ | ||||
| import { Component, forwardRef } from '@angular/core' | ||||
| import { NG_VALUE_ACCESSOR } from '@angular/forms' | ||||
| import { AbstractInputComponent } from '../abstract-input' | ||||
|  | ||||
| @Component({ | ||||
|   providers: [ | ||||
|     { | ||||
|       provide: NG_VALUE_ACCESSOR, | ||||
|       useExisting: forwardRef(() => PasswordComponent), | ||||
|       multi: true, | ||||
|     }, | ||||
|   ], | ||||
|   selector: 'app-input-password', | ||||
|   templateUrl: './password.component.html', | ||||
|   styleUrls: ['./password.component.scss'], | ||||
| }) | ||||
| export class PasswordComponent extends AbstractInputComponent<string> { | ||||
|   constructor() { | ||||
|     super() | ||||
|   } | ||||
| } | ||||
| @@ -7,7 +7,7 @@ | ||||
|       [closeOnSelect]="false" | ||||
|       [clearSearchOnAdd]="true" | ||||
|       [hideSelected]="true" | ||||
|       [addTag]="createTagRef" | ||||
|       [addTag]="allowCreate ? createTagRef : false" | ||||
|       addTagText="Add tag" | ||||
|       i18n-addTagText | ||||
|       (change)="onChange(value)" | ||||
| @@ -31,7 +31,7 @@ | ||||
|       </ng-template> | ||||
|     </ng-select> | ||||
|  | ||||
|     <button class="btn btn-outline-secondary" type="button" (click)="createTag()"> | ||||
|     <button *ngIf="allowCreate" class="btn btn-outline-secondary" type="button" (click)="createTag()"> | ||||
|       <svg class="buttonicon" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#plus" /> | ||||
|       </svg> | ||||
|   | ||||
| @@ -54,6 +54,9 @@ export class TagsComponent implements OnInit, ControlValueAccessor { | ||||
|   @Input() | ||||
|   suggestions: number[] | ||||
|  | ||||
|   @Input() | ||||
|   allowCreate: boolean = true | ||||
|  | ||||
|   value: number[] | ||||
|  | ||||
|   tags: PaperlessTag[] | ||||
|   | ||||
| @@ -120,8 +120,20 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|       backdrop: 'static', | ||||
|     }) | ||||
|     activeModal.componentInstance.dialogMode = 'create' | ||||
|     activeModal.componentInstance.success.subscribe((o) => { | ||||
|       this.reloadData() | ||||
|     activeModal.componentInstance.success.subscribe({ | ||||
|       next: () => { | ||||
|         this.reloadData() | ||||
|         this.toastService.showInfo( | ||||
|           $localize`Successfully created ${this.typeName}.` | ||||
|         ) | ||||
|       }, | ||||
|       error: (e) => { | ||||
|         this.toastService.showInfo( | ||||
|           $localize`Error occurred while creating ${ | ||||
|             this.typeName | ||||
|           } : ${e.toString()}.` | ||||
|         ) | ||||
|       }, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
| @@ -131,8 +143,20 @@ export abstract class ManagementListComponent<T extends ObjectWithId> | ||||
|     }) | ||||
|     activeModal.componentInstance.object = object | ||||
|     activeModal.componentInstance.dialogMode = 'edit' | ||||
|     activeModal.componentInstance.success.subscribe((o) => { | ||||
|       this.reloadData() | ||||
|     activeModal.componentInstance.success.subscribe({ | ||||
|       next: () => { | ||||
|         this.reloadData() | ||||
|         this.toastService.showInfo( | ||||
|           $localize`Successfully updated ${this.typeName}.` | ||||
|         ) | ||||
|       }, | ||||
|       error: (e) => { | ||||
|         this.toastService.showInfo( | ||||
|           $localize`Error occurred while saving ${ | ||||
|             this.typeName | ||||
|           } : ${e.toString()}.` | ||||
|         ) | ||||
|       }, | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,12 +1,17 @@ | ||||
| <app-page-header title="Settings" i18n-title> | ||||
|   <button class="btn btn-sm btn-outline-primary" (click)="tourService.start()"><ng-container i18n>Start tour</ng-container></button> | ||||
|   <a class="btn btn-sm btn-primary ms-3" href="admin/" target="_blank"> | ||||
|       <ng-container i18n>Open Django Admin</ng-container> | ||||
|       <svg class="sidebaricon ms-1" fill="currentColor"> | ||||
|         <use xlink:href="assets/bootstrap-icons.svg#arrow-up-right"/> | ||||
|       </svg> | ||||
|   </a> | ||||
| </app-page-header> | ||||
|  | ||||
| <!-- <p>items per page, documents per view type</p> --> | ||||
| <form [formGroup]="settingsForm" (ngSubmit)="saveSettings()"> | ||||
|  | ||||
|   <ul ngbNav #nav="ngbNav" class="nav-tabs"> | ||||
|     <li [ngbNavItem]="1"> | ||||
|   <ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-tabs"> | ||||
|     <li [ngbNavItem]="SettingsNavIDs.General"> | ||||
|       <a ngbNavLink i18n>General</a> | ||||
|       <ng-template ngbNavContent> | ||||
|  | ||||
| @@ -162,7 +167,7 @@ | ||||
|       </ng-template> | ||||
|     </li> | ||||
|  | ||||
|     <li [ngbNavItem]="2"> | ||||
|     <li [ngbNavItem]="SettingsNavIDs.Notifications"> | ||||
|       <a ngbNavLink i18n>Notifications</a> | ||||
|       <ng-template ngbNavContent> | ||||
|  | ||||
| @@ -180,7 +185,7 @@ | ||||
|       </ng-template> | ||||
|     </li> | ||||
|  | ||||
|     <li [ngbNavItem]="3"> | ||||
|     <li [ngbNavItem]="SettingsNavIDs.SavedViews" (mouseover)="maybeInitializeTab(SettingsNavIDs.SavedViews)" (focusin)="maybeInitializeTab(SettingsNavIDs.SavedViews)"> | ||||
|       <a ngbNavLink i18n>Saved views</a> | ||||
|       <ng-template ngbNavContent> | ||||
|  | ||||
| @@ -210,8 +215,97 @@ | ||||
|               </div> | ||||
|             </div> | ||||
|  | ||||
|             <div *ngIf="savedViews.length == 0" i18n>No saved views defined.</div> | ||||
|             <div *ngIf="savedViews && savedViews.length == 0" i18n>No saved views defined.</div> | ||||
|  | ||||
|             <div *ngIf="!savedViews"> | ||||
|               <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|               <div class="visually-hidden" i18n>Loading...</div> | ||||
|             </div> | ||||
|  | ||||
|         </div> | ||||
|  | ||||
|       </ng-template> | ||||
|     </li> | ||||
|  | ||||
|     <li [ngbNavItem]="SettingsNavIDs.Mail" (mouseover)="maybeInitializeTab(SettingsNavIDs.Mail)" (focusin)="maybeInitializeTab(SettingsNavIDs.Mail)"> | ||||
|       <a ngbNavLink i18n>Mail</a> | ||||
|       <ng-template ngbNavContent> | ||||
|  | ||||
|         <ng-container *ngIf="mailAccounts && mailRules"> | ||||
|           <h4> | ||||
|             <ng-container i18n>Mail accounts</ng-container> | ||||
|             <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailAccount()"> | ||||
|               <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|               </svg> | ||||
|               <ng-container i18n>Add Account</ng-container> | ||||
|             </button> | ||||
|           </h4> | ||||
|           <ul class="list-group" formGroupName="mailAccounts"> | ||||
|  | ||||
|               <li class="list-group-item"> | ||||
|                 <div class="row"> | ||||
|                   <div class="col" i18n>Name</div> | ||||
|                   <div class="col" i18n>Server</div> | ||||
|                   <div class="col" i18n>Actions</div> | ||||
|                 </div> | ||||
|               </li> | ||||
|  | ||||
|               <li *ngFor="let account of mailAccounts" class="list-group-item" [formGroupName]="account.id"> | ||||
|                 <div class="row"> | ||||
|                   <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailAccount(account)">{{account.name}}</button></div> | ||||
|                   <div class="col d-flex align-items-center">{{account.imap_server}}</div> | ||||
|                   <div class="col"> | ||||
|                     <div class="btn-group"> | ||||
|                       <button class="btn btn-sm btn-primary" type="button" (click)="editMailAccount(account)" i18n>Edit</button> | ||||
|                       <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailAccount(account)" i18n>Delete</button> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </li> | ||||
|  | ||||
|               <div *ngIf="mailAccounts.length == 0" i18n>No mail accounts defined.</div> | ||||
|           </ul> | ||||
|  | ||||
|           <h4 class="mt-4"> | ||||
|             <ng-container i18n>Mail rules</ng-container> | ||||
|             <button type="button" class="btn btn-sm btn-primary ms-4" (click)="editMailRule()"> | ||||
|               <svg class="sidebaricon me-1" fill="currentColor"> | ||||
|                 <use xlink:href="assets/bootstrap-icons.svg#plus-circle" /> | ||||
|               </svg> | ||||
|               <ng-container i18n>Add Rule</ng-container> | ||||
|             </button> | ||||
|           </h4> | ||||
|           <ul class="list-group" formGroupName="mailRules"> | ||||
|  | ||||
|               <li class="list-group-item"> | ||||
|                 <div class="row"> | ||||
|                   <div class="col" i18n>Name</div> | ||||
|                   <div class="col" i18n>Account</div> | ||||
|                   <div class="col" i18n>Actions</div> | ||||
|                 </div> | ||||
|               </li> | ||||
|  | ||||
|               <li *ngFor="let rule of mailRules" class="list-group-item" [formGroupName]="rule.id"> | ||||
|                 <div class="row"> | ||||
|                   <div class="col d-flex align-items-center"><button class="btn btn-link p-0" type="button" (click)="editMailRule(rule)">{{rule.name}}</button></div> | ||||
|                   <div class="col d-flex align-items-center">{{(mailAccountService.getCached(rule.account) | async)?.name}}</div> | ||||
|                   <div class="col"> | ||||
|                     <div class="btn-group"> | ||||
|                       <button class="btn btn-sm btn-primary" type="button" (click)="editMailRule(rule)" i18n>Edit</button> | ||||
|                       <button class="btn btn-sm btn-outline-danger" type="button" (click)="deleteMailRule(rule)" i18n>Delete</button> | ||||
|                     </div> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </li> | ||||
|  | ||||
|               <div *ngIf="mailRules.length == 0" i18n>No mail rules defined.</div> | ||||
|           </ul> | ||||
|         </ng-container> | ||||
|  | ||||
|         <div *ngIf="!mailAccounts || !mailRules"> | ||||
|           <div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div> | ||||
|           <div class="visually-hidden" i18n>Loading...</div> | ||||
|         </div> | ||||
|  | ||||
|       </ng-template> | ||||
|   | ||||
| @@ -26,9 +26,26 @@ import { | ||||
|   Subject, | ||||
| } from 'rxjs' | ||||
| import { SETTINGS_KEYS } from 'src/app/data/paperless-uisettings' | ||||
| import { ActivatedRoute } from '@angular/router' | ||||
| import { ActivatedRoute, Router } from '@angular/router' | ||||
| import { ViewportScroller } from '@angular/common' | ||||
| import { TourService } from 'ngx-ui-tour-ng-bootstrap' | ||||
| import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account' | ||||
| import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule' | ||||
| import { MailAccountService } from 'src/app/services/rest/mail-account.service' | ||||
| import { MailRuleService } from 'src/app/services/rest/mail-rule.service' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component' | ||||
| import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component' | ||||
| import { ConfirmDialogComponent } from '../../common/confirm-dialog/confirm-dialog.component' | ||||
| import { NgbNavChangeEvent } from '@ng-bootstrap/ng-bootstrap' | ||||
|  | ||||
| enum SettingsNavIDs { | ||||
|   General = 1, | ||||
|   Notifications = 2, | ||||
|   SavedViews = 3, | ||||
|   Mail = 4, | ||||
|   UsersGroups = 5, | ||||
| } | ||||
|  | ||||
| @Component({ | ||||
|   selector: 'app-settings', | ||||
| @@ -38,8 +55,14 @@ import { TourService } from 'ngx-ui-tour-ng-bootstrap' | ||||
| export class SettingsComponent | ||||
|   implements OnInit, AfterViewInit, OnDestroy, DirtyComponent | ||||
| { | ||||
|   SettingsNavIDs = SettingsNavIDs | ||||
|   activeNavID: number | ||||
|  | ||||
|   savedViewGroup = new FormGroup({}) | ||||
|  | ||||
|   mailAccountGroup = new FormGroup({}) | ||||
|   mailRuleGroup = new FormGroup({}) | ||||
|  | ||||
|   settingsForm = new FormGroup({ | ||||
|     bulkEditConfirmationDialogs: new FormControl(null), | ||||
|     bulkEditApplyOnClose: new FormControl(null), | ||||
| @@ -50,20 +73,28 @@ export class SettingsComponent | ||||
|     darkModeInvertThumbs: new FormControl(null), | ||||
|     themeColor: new FormControl(null), | ||||
|     useNativePdfViewer: new FormControl(null), | ||||
|     savedViews: this.savedViewGroup, | ||||
|     displayLanguage: new FormControl(null), | ||||
|     dateLocale: new FormControl(null), | ||||
|     dateFormat: new FormControl(null), | ||||
|     commentsEnabled: new FormControl(null), | ||||
|     updateCheckingEnabled: new FormControl(null), | ||||
|  | ||||
|     notificationsConsumerNewDocument: new FormControl(null), | ||||
|     notificationsConsumerSuccess: new FormControl(null), | ||||
|     notificationsConsumerFailed: new FormControl(null), | ||||
|     notificationsConsumerSuppressOnDashboard: new FormControl(null), | ||||
|     commentsEnabled: new FormControl(null), | ||||
|     updateCheckingEnabled: new FormControl(null), | ||||
|  | ||||
|     savedViews: this.savedViewGroup, | ||||
|  | ||||
|     mailAccounts: this.mailAccountGroup, | ||||
|     mailRules: this.mailRuleGroup, | ||||
|   }) | ||||
|  | ||||
|   savedViews: PaperlessSavedView[] | ||||
|  | ||||
|   mailAccounts: PaperlessMailAccount[] | ||||
|   mailRules: PaperlessMailRule[] | ||||
|  | ||||
|   store: BehaviorSubject<any> | ||||
|   storeSub: Subscription | ||||
|   isDirty$: Observable<boolean> | ||||
| @@ -81,19 +112,40 @@ export class SettingsComponent | ||||
|  | ||||
|   constructor( | ||||
|     public savedViewService: SavedViewService, | ||||
|     public mailAccountService: MailAccountService, | ||||
|     public mailRuleService: MailRuleService, | ||||
|     private documentListViewService: DocumentListViewService, | ||||
|     private toastService: ToastService, | ||||
|     private settings: SettingsService, | ||||
|     @Inject(LOCALE_ID) public currentLocale: string, | ||||
|     private viewportScroller: ViewportScroller, | ||||
|     private activatedRoute: ActivatedRoute, | ||||
|     public readonly tourService: TourService | ||||
|     private router: Router, | ||||
|     public readonly tourService: TourService, | ||||
|     private modalService: NgbModal | ||||
|   ) { | ||||
|     this.settings.settingsSaved.subscribe(() => { | ||||
|       if (!this.savePending) this.initialize() | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   ngOnInit() { | ||||
|     this.initialize() | ||||
|  | ||||
|     this.activatedRoute.paramMap.subscribe((paramMap) => { | ||||
|       const section = paramMap.get('section') | ||||
|       if (section) { | ||||
|         const navIDKey: string = Object.keys(SettingsNavIDs).find( | ||||
|           (navID) => navID.toLowerCase() == section | ||||
|         ) | ||||
|         if (navIDKey) { | ||||
|           this.activeNavID = SettingsNavIDs[navIDKey] | ||||
|           this.maybeInitializeTab(this.activeNavID) | ||||
|         } | ||||
|       } | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   ngAfterViewInit(): void { | ||||
|     if (this.activatedRoute.snapshot.fragment) { | ||||
|       this.viewportScroller.scrollToAnchor( | ||||
| @@ -123,10 +175,13 @@ export class SettingsComponent | ||||
|       useNativePdfViewer: this.settings.get( | ||||
|         SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER | ||||
|       ), | ||||
|       savedViews: {}, | ||||
|       displayLanguage: this.settings.getLanguage(), | ||||
|       dateLocale: this.settings.get(SETTINGS_KEYS.DATE_LOCALE), | ||||
|       dateFormat: this.settings.get(SETTINGS_KEYS.DATE_FORMAT), | ||||
|       commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED), | ||||
|       updateCheckingEnabled: this.settings.get( | ||||
|         SETTINGS_KEYS.UPDATE_CHECKING_ENABLED | ||||
|       ), | ||||
|       notificationsConsumerNewDocument: this.settings.get( | ||||
|         SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT | ||||
|       ), | ||||
| @@ -139,41 +194,147 @@ export class SettingsComponent | ||||
|       notificationsConsumerSuppressOnDashboard: this.settings.get( | ||||
|         SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD | ||||
|       ), | ||||
|       commentsEnabled: this.settings.get(SETTINGS_KEYS.COMMENTS_ENABLED), | ||||
|       updateCheckingEnabled: this.settings.get( | ||||
|         SETTINGS_KEYS.UPDATE_CHECKING_ENABLED | ||||
|       ), | ||||
|       savedViews: {}, | ||||
|       mailAccounts: {}, | ||||
|       mailRules: {}, | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ngOnInit() { | ||||
|     this.savedViewService.listAll().subscribe((r) => { | ||||
|       this.savedViews = r.results | ||||
|       this.initialize() | ||||
|     }) | ||||
|   onNavChange(navChangeEvent: NgbNavChangeEvent) { | ||||
|     this.maybeInitializeTab(navChangeEvent.nextId) | ||||
|     const [foundNavIDkey, foundNavIDValue] = Object.entries( | ||||
|       SettingsNavIDs | ||||
|     ).find(([navIDkey, navIDValue]) => navIDValue == navChangeEvent.nextId) | ||||
|     if (foundNavIDkey) | ||||
|       // if its dirty we need to wait for confirmation | ||||
|       this.router | ||||
|         .navigate(['settings', foundNavIDkey.toLowerCase()]) | ||||
|         .then((navigated) => { | ||||
|           if (!navigated && this.isDirty) { | ||||
|             this.activeNavID = navChangeEvent.activeId | ||||
|           } else if (navigated && this.isDirty) { | ||||
|             this.initialize() | ||||
|           } | ||||
|         }) | ||||
|   } | ||||
|  | ||||
|   initialize() { | ||||
|   // Load tab contents 'on demand', either on mouseover or focusin (i.e. before click) or called from nav change event | ||||
|   maybeInitializeTab(navID: number): void { | ||||
|     if (navID == SettingsNavIDs.SavedViews && !this.savedViews) { | ||||
|       this.savedViewService.listAll().subscribe((r) => { | ||||
|         this.savedViews = r.results | ||||
|         this.initialize(false) | ||||
|       }) | ||||
|     } else if ( | ||||
|       navID == SettingsNavIDs.Mail && | ||||
|       (!this.mailAccounts || !this.mailRules) | ||||
|     ) { | ||||
|       this.mailAccountService.listAll().subscribe((r) => { | ||||
|         this.mailAccounts = r.results | ||||
|  | ||||
|         this.mailRuleService.listAll().subscribe((r) => { | ||||
|           this.mailRules = r.results | ||||
|           this.initialize(false) | ||||
|         }) | ||||
|       }) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   initialize(resetSettings: boolean = true) { | ||||
|     this.unsubscribeNotifier.next(true) | ||||
|  | ||||
|     const currentFormValue = this.settingsForm.value | ||||
|  | ||||
|     let storeData = this.getCurrentSettings() | ||||
|  | ||||
|     for (let view of this.savedViews) { | ||||
|       storeData.savedViews[view.id.toString()] = { | ||||
|         id: view.id, | ||||
|         name: view.name, | ||||
|         show_on_dashboard: view.show_on_dashboard, | ||||
|         show_in_sidebar: view.show_in_sidebar, | ||||
|     if (this.savedViews) { | ||||
|       for (let view of this.savedViews) { | ||||
|         storeData.savedViews[view.id.toString()] = { | ||||
|           id: view.id, | ||||
|           name: view.name, | ||||
|           show_on_dashboard: view.show_on_dashboard, | ||||
|           show_in_sidebar: view.show_in_sidebar, | ||||
|         } | ||||
|         this.savedViewGroup.addControl( | ||||
|           view.id.toString(), | ||||
|           new FormGroup({ | ||||
|             id: new FormControl(null), | ||||
|             name: new FormControl(null), | ||||
|             show_on_dashboard: new FormControl(null), | ||||
|             show_in_sidebar: new FormControl(null), | ||||
|           }) | ||||
|         ) | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (this.mailAccounts && this.mailRules) { | ||||
|       for (let account of this.mailAccounts) { | ||||
|         storeData.mailAccounts[account.id.toString()] = { | ||||
|           id: account.id, | ||||
|           name: account.name, | ||||
|           imap_server: account.imap_server, | ||||
|           imap_port: account.imap_port, | ||||
|           imap_security: account.imap_security, | ||||
|           username: account.username, | ||||
|           password: account.password, | ||||
|           character_set: account.character_set, | ||||
|         } | ||||
|         this.mailAccountGroup.addControl( | ||||
|           account.id.toString(), | ||||
|           new FormGroup({ | ||||
|             id: new FormControl(null), | ||||
|             name: new FormControl(null), | ||||
|             imap_server: new FormControl(null), | ||||
|             imap_port: new FormControl(null), | ||||
|             imap_security: new FormControl(null), | ||||
|             username: new FormControl(null), | ||||
|             password: new FormControl(null), | ||||
|             character_set: new FormControl(null), | ||||
|           }) | ||||
|         ) | ||||
|       } | ||||
|  | ||||
|       for (let rule of this.mailRules) { | ||||
|         storeData.mailRules[rule.id.toString()] = { | ||||
|           name: rule.name, | ||||
|           account: rule.account, | ||||
|           folder: rule.folder, | ||||
|           filter_from: rule.filter_from, | ||||
|           filter_subject: rule.filter_subject, | ||||
|           filter_body: rule.filter_body, | ||||
|           filter_attachment_filename: rule.filter_attachment_filename, | ||||
|           maximum_age: rule.maximum_age, | ||||
|           attachment_type: rule.attachment_type, | ||||
|           action: rule.action, | ||||
|           action_parameter: rule.action_parameter, | ||||
|           assign_title_from: rule.assign_title_from, | ||||
|           assign_tags: rule.assign_tags, | ||||
|           assign_document_type: rule.assign_document_type, | ||||
|           assign_correspondent_from: rule.assign_correspondent_from, | ||||
|           assign_correspondent: rule.assign_correspondent, | ||||
|         } | ||||
|         this.mailRuleGroup.addControl( | ||||
|           rule.id.toString(), | ||||
|           new FormGroup({ | ||||
|             name: new FormControl(null), | ||||
|             account: new FormControl(null), | ||||
|             folder: new FormControl(null), | ||||
|             filter_from: new FormControl(null), | ||||
|             filter_subject: new FormControl(null), | ||||
|             filter_body: new FormControl(null), | ||||
|             filter_attachment_filename: new FormControl(null), | ||||
|             maximum_age: new FormControl(null), | ||||
|             attachment_type: new FormControl(null), | ||||
|             action: new FormControl(null), | ||||
|             action_parameter: new FormControl(null), | ||||
|             assign_title_from: new FormControl(null), | ||||
|             assign_tags: new FormControl(null), | ||||
|             assign_document_type: new FormControl(null), | ||||
|             assign_correspondent_from: new FormControl(null), | ||||
|             assign_correspondent: new FormControl(null), | ||||
|           }) | ||||
|         ) | ||||
|       } | ||||
|       this.savedViewGroup.addControl( | ||||
|         view.id.toString(), | ||||
|         new FormGroup({ | ||||
|           id: new FormControl(null), | ||||
|           name: new FormControl(null), | ||||
|           show_on_dashboard: new FormControl(null), | ||||
|           show_in_sidebar: new FormControl(null), | ||||
|         }) | ||||
|       ) | ||||
|     } | ||||
|  | ||||
|     this.store = new BehaviorSubject(storeData) | ||||
| @@ -202,6 +363,11 @@ export class SettingsComponent | ||||
|           this.settingsForm.get('themeColor').value | ||||
|         ) | ||||
|       }) | ||||
|  | ||||
|     if (!resetSettings && currentFormValue) { | ||||
|       // prevents loss of unsaved changes | ||||
|       this.settingsForm.patchValue(currentFormValue) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   ngOnDestroy() { | ||||
| @@ -372,4 +538,121 @@ export class SettingsComponent | ||||
|   clearThemeColor() { | ||||
|     this.settingsForm.get('themeColor').patchValue('') | ||||
|   } | ||||
|  | ||||
|   editMailAccount(account: PaperlessMailAccount) { | ||||
|     const modal = this.modalService.open(MailAccountEditDialogComponent, { | ||||
|       backdrop: 'static', | ||||
|       size: 'xl', | ||||
|     }) | ||||
|     modal.componentInstance.dialogMode = account ? 'edit' : 'create' | ||||
|     modal.componentInstance.object = account | ||||
|     modal.componentInstance.success | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe({ | ||||
|         next: (newMailAccount) => { | ||||
|           this.toastService.showInfo( | ||||
|             $localize`Saved account "${newMailAccount.name}".` | ||||
|           ) | ||||
|           this.mailAccountService.clearCache() | ||||
|           this.mailAccountService.listAll().subscribe((r) => { | ||||
|             this.mailAccounts = r.results | ||||
|             this.initialize() | ||||
|           }) | ||||
|         }, | ||||
|         error: (e) => { | ||||
|           this.toastService.showError( | ||||
|             $localize`Error saving account: ${e.toString()}.` | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   deleteMailAccount(account: PaperlessMailAccount) { | ||||
|     const modal = this.modalService.open(ConfirmDialogComponent, { | ||||
|       backdrop: 'static', | ||||
|     }) | ||||
|     modal.componentInstance.title = $localize`Confirm delete mail account` | ||||
|     modal.componentInstance.messageBold = $localize`This operation will permanently this mail account.` | ||||
|     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.mailAccountService.delete(account).subscribe({ | ||||
|         next: () => { | ||||
|           modal.close() | ||||
|           this.toastService.showInfo($localize`Deleted mail account`) | ||||
|           this.mailAccountService.clearCache() | ||||
|           this.mailAccountService.listAll().subscribe((r) => { | ||||
|             this.mailAccounts = r.results | ||||
|             this.initialize() | ||||
|           }) | ||||
|         }, | ||||
|         error: (e) => { | ||||
|           this.toastService.showError( | ||||
|             $localize`Error deleting mail account: ${e.toString()}.` | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   editMailRule(rule: PaperlessMailRule) { | ||||
|     const modal = this.modalService.open(MailRuleEditDialogComponent, { | ||||
|       backdrop: 'static', | ||||
|       size: 'xl', | ||||
|     }) | ||||
|     modal.componentInstance.dialogMode = rule ? 'edit' : 'create' | ||||
|     modal.componentInstance.object = rule | ||||
|     modal.componentInstance.success | ||||
|       .pipe(takeUntil(this.unsubscribeNotifier)) | ||||
|       .subscribe({ | ||||
|         next: (newMailRule) => { | ||||
|           this.toastService.showInfo( | ||||
|             $localize`Saved rule "${newMailRule.name}".` | ||||
|           ) | ||||
|           this.mailRuleService.clearCache() | ||||
|           this.mailRuleService.listAll().subscribe((r) => { | ||||
|             this.mailRules = r.results | ||||
|  | ||||
|             this.initialize() | ||||
|           }) | ||||
|         }, | ||||
|         error: (e) => { | ||||
|           this.toastService.showError( | ||||
|             $localize`Error saving rule: ${e.toString()}.` | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|   } | ||||
|  | ||||
|   deleteMailRule(rule: PaperlessMailRule) { | ||||
|     const modal = this.modalService.open(ConfirmDialogComponent, { | ||||
|       backdrop: 'static', | ||||
|     }) | ||||
|     modal.componentInstance.title = $localize`Confirm delete mail rule` | ||||
|     modal.componentInstance.messageBold = $localize`This operation will permanently this mail rule.` | ||||
|     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.mailRuleService.delete(rule).subscribe({ | ||||
|         next: () => { | ||||
|           modal.close() | ||||
|           this.toastService.showInfo($localize`Deleted mail rule`) | ||||
|           this.mailRuleService.clearCache() | ||||
|           this.mailRuleService.listAll().subscribe((r) => { | ||||
|             this.mailRules = r.results | ||||
|             this.initialize() | ||||
|           }) | ||||
|         }, | ||||
|         error: (e) => { | ||||
|           this.toastService.showError( | ||||
|             $localize`Error deleting mail rule: ${e.toString()}.` | ||||
|           ) | ||||
|         }, | ||||
|       }) | ||||
|     }) | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										23
									
								
								src-ui/src/app/data/paperless-mail-account.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src-ui/src/app/data/paperless-mail-account.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | ||||
| import { ObjectWithId } from './object-with-id' | ||||
|  | ||||
| export enum IMAPSecurity { | ||||
|   None = 1, | ||||
|   SSL = 2, | ||||
|   STARTTLS = 3, | ||||
| } | ||||
|  | ||||
| export interface PaperlessMailAccount extends ObjectWithId { | ||||
|   name: string | ||||
|  | ||||
|   imap_server: string | ||||
|  | ||||
|   imap_port: number | ||||
|  | ||||
|   imap_security: IMAPSecurity | ||||
|  | ||||
|   username: string | ||||
|  | ||||
|   password: string | ||||
|  | ||||
|   character_set?: string | ||||
| } | ||||
							
								
								
									
										60
									
								
								src-ui/src/app/data/paperless-mail-rule.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src-ui/src/app/data/paperless-mail-rule.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,60 @@ | ||||
| import { ObjectWithId } from './object-with-id' | ||||
|  | ||||
| export enum MailFilterAttachmentType { | ||||
|   Attachments = 1, | ||||
|   Everything = 2, | ||||
| } | ||||
|  | ||||
| export enum MailAction { | ||||
|   Delete = 1, | ||||
|   Move = 2, | ||||
|   MarkRead = 3, | ||||
|   Flag = 4, | ||||
|   Tag = 5, | ||||
| } | ||||
|  | ||||
| export enum MailMetadataTitleOption { | ||||
|   FromSubject = 1, | ||||
|   FromFilename = 2, | ||||
| } | ||||
|  | ||||
| export enum MailMetadataCorrespondentOption { | ||||
|   FromNothing = 1, | ||||
|   FromEmail = 2, | ||||
|   FromName = 3, | ||||
|   FromCustom = 4, | ||||
| } | ||||
|  | ||||
| export interface PaperlessMailRule extends ObjectWithId { | ||||
|   name: string | ||||
|  | ||||
|   account: number // PaperlessMailAccount.id | ||||
|  | ||||
|   folder: string | ||||
|  | ||||
|   filter_from: string | ||||
|  | ||||
|   filter_subject: string | ||||
|  | ||||
|   filter_body: string | ||||
|  | ||||
|   filter_attachment_filename: string | ||||
|  | ||||
|   maximum_age: number | ||||
|  | ||||
|   attachment_type: MailFilterAttachmentType | ||||
|  | ||||
|   action: MailAction | ||||
|  | ||||
|   action_parameter?: string | ||||
|  | ||||
|   assign_title_from: MailMetadataTitleOption | ||||
|  | ||||
|   assign_tags?: number[] // PaperlessTag.id | ||||
|  | ||||
|   assign_document_type?: number // PaperlessDocumentType.id | ||||
|  | ||||
|   assign_correspondent_from?: MailMetadataCorrespondentOption | ||||
|  | ||||
|   assign_correspondent?: number // PaperlessCorrespondent.id | ||||
| } | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { Injectable } from '@angular/core' | ||||
| import { DirtyCheckGuard } from '@ngneat/dirty-check-forms' | ||||
| import { Observable, Subject } from 'rxjs' | ||||
| import { map } from 'rxjs/operators' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component' | ||||
|  | ||||
|   | ||||
							
								
								
									
										51
									
								
								src-ui/src/app/services/rest/mail-account.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src-ui/src/app/services/rest/mail-account.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import { HttpClient } from '@angular/common/http' | ||||
| import { Injectable } from '@angular/core' | ||||
| import { combineLatest, Observable } from 'rxjs' | ||||
| import { tap } from 'rxjs/operators' | ||||
| import { PaperlessMailAccount } from 'src/app/data/paperless-mail-account' | ||||
| import { AbstractPaperlessService } from './abstract-paperless-service' | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class MailAccountService extends AbstractPaperlessService<PaperlessMailAccount> { | ||||
|   loading: boolean | ||||
|  | ||||
|   constructor(http: HttpClient) { | ||||
|     super(http, 'mail_accounts') | ||||
|   } | ||||
|  | ||||
|   private reload() { | ||||
|     this.loading = true | ||||
|     this.listAll().subscribe((r) => { | ||||
|       this.mailAccounts = r.results | ||||
|       this.loading = false | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   private mailAccounts: PaperlessMailAccount[] = [] | ||||
|  | ||||
|   get allAccounts() { | ||||
|     return this.mailAccounts | ||||
|   } | ||||
|  | ||||
|   create(o: PaperlessMailAccount) { | ||||
|     return super.create(o).pipe(tap(() => this.reload())) | ||||
|   } | ||||
|  | ||||
|   update(o: PaperlessMailAccount) { | ||||
|     return super.update(o).pipe(tap(() => this.reload())) | ||||
|   } | ||||
|  | ||||
|   patchMany( | ||||
|     objects: PaperlessMailAccount[] | ||||
|   ): Observable<PaperlessMailAccount[]> { | ||||
|     return combineLatest(objects.map((o) => super.patch(o))).pipe( | ||||
|       tap(() => this.reload()) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   delete(o: PaperlessMailAccount) { | ||||
|     return super.delete(o).pipe(tap(() => this.reload())) | ||||
|   } | ||||
| } | ||||
							
								
								
									
										49
									
								
								src-ui/src/app/services/rest/mail-rule.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src-ui/src/app/services/rest/mail-rule.service.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,49 @@ | ||||
| import { HttpClient } from '@angular/common/http' | ||||
| import { Injectable } from '@angular/core' | ||||
| import { combineLatest, Observable } from 'rxjs' | ||||
| import { tap } from 'rxjs/operators' | ||||
| import { PaperlessMailRule } from 'src/app/data/paperless-mail-rule' | ||||
| import { AbstractPaperlessService } from './abstract-paperless-service' | ||||
|  | ||||
| @Injectable({ | ||||
|   providedIn: 'root', | ||||
| }) | ||||
| export class MailRuleService extends AbstractPaperlessService<PaperlessMailRule> { | ||||
|   loading: boolean | ||||
|  | ||||
|   constructor(http: HttpClient) { | ||||
|     super(http, 'mail_rules') | ||||
|   } | ||||
|  | ||||
|   private reload() { | ||||
|     this.loading = true | ||||
|     this.listAll().subscribe((r) => { | ||||
|       this.mailRules = r.results | ||||
|       this.loading = false | ||||
|     }) | ||||
|   } | ||||
|  | ||||
|   private mailRules: PaperlessMailRule[] = [] | ||||
|  | ||||
|   get allRules() { | ||||
|     return this.mailRules | ||||
|   } | ||||
|  | ||||
|   create(o: PaperlessMailRule) { | ||||
|     return super.create(o).pipe(tap(() => this.reload())) | ||||
|   } | ||||
|  | ||||
|   update(o: PaperlessMailRule) { | ||||
|     return super.update(o).pipe(tap(() => this.reload())) | ||||
|   } | ||||
|  | ||||
|   patchMany(objects: PaperlessMailRule[]): Observable<PaperlessMailRule[]> { | ||||
|     return combineLatest(objects.map((o) => super.patch(o))).pipe( | ||||
|       tap(() => this.reload()) | ||||
|     ) | ||||
|   } | ||||
|  | ||||
|   delete(o: PaperlessMailRule) { | ||||
|     return super.delete(o).pipe(tap(() => this.reload())) | ||||
|   } | ||||
| } | ||||
| @@ -242,10 +242,12 @@ a, a:hover, | ||||
|   } | ||||
|  | ||||
|   .input-group { | ||||
|     .ng-select-container { | ||||
|       height: 100%; | ||||
|       border-top-right-radius: 0; | ||||
|       border-bottom-right-radius: 0; | ||||
|     ng-select:not(:last-child) { | ||||
|       .ng-select-container { | ||||
|         height: 100%; | ||||
|         border-top-right-radius: 0; | ||||
|         border-bottom-right-radius: 0; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon