mirror of
				https://github.com/paperless-ngx/paperless-ngx.git
				synced 2025-10-26 03:36:08 -05:00 
			
		
		
		
	Merge pull request #1868 from paperless-ngx/fix/issue-1866
Fix: independent control of saved views
This commit is contained in:
		| @@ -15,6 +15,7 @@ import { DirtyFormGuard } from './guards/dirty-form.guard' | ||||
| import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component' | ||||
| import { TasksComponent } from './components/manage/tasks/tasks.component' | ||||
| import { DirtyDocGuard } from './guards/dirty-doc.guard' | ||||
| import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard' | ||||
|  | ||||
| const routes: Routes = [ | ||||
|   { path: '', redirectTo: 'dashboard', pathMatch: 'full' }, | ||||
| @@ -24,8 +25,16 @@ const routes: Routes = [ | ||||
|     canDeactivate: [DirtyDocGuard], | ||||
|     children: [ | ||||
|       { path: 'dashboard', component: DashboardComponent }, | ||||
|       { path: 'documents', component: DocumentListComponent }, | ||||
|       { path: 'view/:id', component: DocumentListComponent }, | ||||
|       { | ||||
|         path: 'documents', | ||||
|         component: DocumentListComponent, | ||||
|         canDeactivate: [DirtySavedViewGuard], | ||||
|       }, | ||||
|       { | ||||
|         path: 'view/:id', | ||||
|         component: DocumentListComponent, | ||||
|         canDeactivate: [DirtySavedViewGuard], | ||||
|       }, | ||||
|       { path: 'documents/:id', component: DocumentDetailComponent }, | ||||
|       { path: 'asn/:id', component: DocumentAsnComponent }, | ||||
|       { path: 'tags', component: TagListComponent }, | ||||
|   | ||||
| @@ -69,6 +69,7 @@ import { ColorComponent } from './components/common/input/color/color.component' | ||||
| import { DocumentAsnComponent } from './components/document-asn/document-asn.component' | ||||
| import { DocumentCommentsComponent } from './components/document-comments/document-comments.component' | ||||
| import { DirtyDocGuard } from './guards/dirty-doc.guard' | ||||
| import { DirtySavedViewGuard } from './guards/dirty-saved-view.guard' | ||||
| import { StoragePathListComponent } from './components/manage/storage-path-list/storage-path-list.component' | ||||
| import { StoragePathEditDialogComponent } from './components/common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component' | ||||
| import { SettingsService } from './services/settings.service' | ||||
| @@ -215,6 +216,7 @@ function initializeApp(settings: SettingsService) { | ||||
|     { provide: NgbDateAdapter, useClass: ISODateAdapter }, | ||||
|     { provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter }, | ||||
|     DirtyDocGuard, | ||||
|     DirtySavedViewGuard, | ||||
|   ], | ||||
|   bootstrap: [AppComponent], | ||||
| }) | ||||
|   | ||||
| @@ -16,4 +16,7 @@ | ||||
|         <ngb-progressbar *ngIf="!confirmButtonEnabled" style="height: 1px;" type="dark" [max]="secondsTotal" [value]="seconds"></ngb-progressbar> | ||||
|         <span class="visually-hidden">{{ seconds | number: '1.0-0' }} seconds</span> | ||||
|       </button> | ||||
|       <button *ngIf="alternativeBtnCaption" type="button" class="btn" [class]="alternativeBtnClass" (click)="alternative()" [disabled]="!alternativeButtonEnabled || !buttonsEnabled"> | ||||
|         {{alternativeBtnCaption}} | ||||
|       </button> | ||||
|     </div> | ||||
|   | ||||
| @@ -13,6 +13,9 @@ export class ConfirmDialogComponent { | ||||
|   @Output() | ||||
|   public confirmClicked = new EventEmitter() | ||||
|  | ||||
|   @Output() | ||||
|   public alternativeClicked = new EventEmitter() | ||||
|  | ||||
|   @Input() | ||||
|   title = $localize`Confirmation` | ||||
|  | ||||
| @@ -28,14 +31,22 @@ export class ConfirmDialogComponent { | ||||
|   @Input() | ||||
|   btnCaption = $localize`Confirm` | ||||
|  | ||||
|   @Input() | ||||
|   alternativeBtnClass = 'btn-secondary' | ||||
|  | ||||
|   @Input() | ||||
|   alternativeBtnCaption | ||||
|  | ||||
|   @Input() | ||||
|   buttonsEnabled = true | ||||
|  | ||||
|   confirmButtonEnabled = true | ||||
|   alternativeButtonEnabled = true | ||||
|   seconds = 0 | ||||
|   secondsTotal = 0 | ||||
|  | ||||
|   confirmSubject: Subject<boolean> | ||||
|   alternativeSubject: Subject<boolean> | ||||
|  | ||||
|   delayConfirm(seconds: number) { | ||||
|     const refreshInterval = 0.15 // s | ||||
| @@ -68,4 +79,10 @@ export class ConfirmDialogComponent { | ||||
|     this.confirmSubject?.next(true) | ||||
|     this.confirmSubject?.complete() | ||||
|   } | ||||
|  | ||||
|   alternative() { | ||||
|     this.alternativeClicked.emit() | ||||
|     this.alternativeSubject?.next(true) | ||||
|     this.alternativeSubject?.complete() | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -60,14 +60,19 @@ | ||||
|   </div> | ||||
|  | ||||
|   <div class="btn-group ms-2 flex-fill" ngbDropdown role="group"> | ||||
|     <button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle i18n>Views</button> | ||||
|     <button class="btn btn-sm btn-outline-primary dropdown-toggle flex-fill" tourAnchor="tour.documents-views" ngbDropdownToggle> | ||||
|       <ng-container i18n>Views</ng-container> | ||||
|       <div *ngIf="savedViewIsModified" class="position-absolute top-0 start-100 p-2 translate-middle badge bg-secondary border border-light rounded-circle"> | ||||
|         <span class="visually-hidden">selected</span> | ||||
|       </div> | ||||
|     </button> | ||||
|     <div class="dropdown-menu shadow dropdown-menu-right" ngbDropdownMenu> | ||||
|       <ng-container *ngIf="!list.activeSavedViewId"> | ||||
|         <button ngbDropdownItem *ngFor="let view of savedViewService.allViews" (click)="loadViewConfig(view.id)">{{view.name}}</button> | ||||
|         <div class="dropdown-divider" *ngIf="savedViewService.allViews.length > 0"></div> | ||||
|       </ng-container> | ||||
|  | ||||
|       <button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" i18n>Save "{{list.activeSavedViewTitle}}"</button> | ||||
|       <button ngbDropdownItem (click)="saveViewConfig()" *ngIf="list.activeSavedViewId" [disabled]="!savedViewIsModified" i18n>Save "{{list.activeSavedViewTitle}}"</button> | ||||
|       <button ngbDropdownItem (click)="saveViewConfigAs()" i18n>Save as...</button> | ||||
|     </div> | ||||
|   </div> | ||||
|   | ||||
| @@ -9,7 +9,11 @@ import { | ||||
| import { ActivatedRoute, convertToParamMap, Router } from '@angular/router' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { filter, first, map, Subject, switchMap, takeUntil } from 'rxjs' | ||||
| import { FilterRule, isFullTextFilterRule } from 'src/app/data/filter-rule' | ||||
| import { | ||||
|   FilterRule, | ||||
|   filterRulesDiffer, | ||||
|   isFullTextFilterRule, | ||||
| } from 'src/app/data/filter-rule' | ||||
| import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type' | ||||
| import { PaperlessDocument } from 'src/app/data/paperless-document' | ||||
| import { PaperlessSavedView } from 'src/app/data/paperless-saved-view' | ||||
| @@ -54,15 +58,36 @@ export class DocumentListComponent implements OnInit, OnDestroy { | ||||
|   displayMode = 'smallCards' // largeCards, smallCards, details | ||||
|  | ||||
|   unmodifiedFilterRules: FilterRule[] = [] | ||||
|   private unmodifiedSavedView: PaperlessSavedView | ||||
|  | ||||
|   private unsubscribeNotifier: Subject<any> = new Subject() | ||||
|  | ||||
|   get savedViewIsModified(): boolean { | ||||
|     if (!this.list.activeSavedViewId || !this.unmodifiedSavedView) return false | ||||
|     else { | ||||
|       return ( | ||||
|         this.unmodifiedSavedView.sort_field !== this.list.sortField || | ||||
|         this.unmodifiedSavedView.sort_reverse !== this.list.sortReverse || | ||||
|         filterRulesDiffer( | ||||
|           this.unmodifiedSavedView.filter_rules, | ||||
|           this.list.filterRules | ||||
|         ) | ||||
|       ) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   get isFiltered() { | ||||
|     return this.list.filterRules?.length > 0 | ||||
|   } | ||||
|  | ||||
|   getTitle() { | ||||
|     return this.list.activeSavedViewTitle || $localize`Documents` | ||||
|     let title = this.list.activeSavedViewTitle | ||||
|     if (title && this.savedViewIsModified) { | ||||
|       title += '*' | ||||
|     } else if (!title) { | ||||
|       title = $localize`Documents` | ||||
|     } | ||||
|     return title | ||||
|   } | ||||
|  | ||||
|   getSortFields() { | ||||
| @@ -122,7 +147,7 @@ export class DocumentListComponent implements OnInit, OnDestroy { | ||||
|           this.router.navigate(['404']) | ||||
|           return | ||||
|         } | ||||
|  | ||||
|         this.unmodifiedSavedView = view | ||||
|         this.list.activateSavedViewWithQueryParams( | ||||
|           view, | ||||
|           convertToParamMap(this.route.snapshot.queryParams) | ||||
| @@ -165,7 +190,8 @@ export class DocumentListComponent implements OnInit, OnDestroy { | ||||
|       this.savedViewService | ||||
|         .patch(savedView) | ||||
|         .pipe(first()) | ||||
|         .subscribe((result) => { | ||||
|         .subscribe((view) => { | ||||
|           this.unmodifiedSavedView = view | ||||
|           this.toastService.showInfo( | ||||
|             $localize`View "${this.list.activeSavedViewTitle}" saved successfully.` | ||||
|           ) | ||||
| @@ -179,6 +205,7 @@ export class DocumentListComponent implements OnInit, OnDestroy { | ||||
|       .getCached(viewID) | ||||
|       .pipe(first()) | ||||
|       .subscribe((view) => { | ||||
|         this.unmodifiedSavedView = view | ||||
|         this.list.activateSavedView(view) | ||||
|         this.list.reload() | ||||
|       }) | ||||
|   | ||||
							
								
								
									
										51
									
								
								src-ui/src/app/guards/dirty-saved-view.guard.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src-ui/src/app/guards/dirty-saved-view.guard.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | ||||
| import { CanDeactivate } from '@angular/router' | ||||
| import { Injectable } from '@angular/core' | ||||
| import { first, Observable, Subject } from 'rxjs' | ||||
| import { DocumentListComponent } from '../components/document-list/document-list.component' | ||||
| import { NgbModal } from '@ng-bootstrap/ng-bootstrap' | ||||
| import { ConfirmDialogComponent } from '../components/common/confirm-dialog/confirm-dialog.component' | ||||
|  | ||||
| @Injectable() | ||||
| export class DirtySavedViewGuard | ||||
|   implements CanDeactivate<DocumentListComponent> | ||||
| { | ||||
|   constructor(private modalService: NgbModal) {} | ||||
|  | ||||
|   canDeactivate( | ||||
|     component: DocumentListComponent | ||||
|   ): boolean | Observable<boolean> { | ||||
|     return component.savedViewIsModified ? this.warn(component) : true | ||||
|   } | ||||
|  | ||||
|   warn(component: DocumentListComponent) { | ||||
|     let modal = this.modalService.open(ConfirmDialogComponent, { | ||||
|       backdrop: 'static', | ||||
|     }) | ||||
|     modal.componentInstance.title = $localize`Unsaved Changes` | ||||
|     modal.componentInstance.messageBold = | ||||
|       $localize`You have unsaved changes to the saved view` + | ||||
|       ' "' + | ||||
|       component.getTitle() | ||||
|     ;('".') | ||||
|     modal.componentInstance.message = $localize`Are you sure you want to close this saved view?` | ||||
|     modal.componentInstance.btnClass = 'btn-secondary' | ||||
|     modal.componentInstance.btnCaption = $localize`Close` | ||||
|     modal.componentInstance.alternativeBtnClass = 'btn-primary' | ||||
|     modal.componentInstance.alternativeBtnCaption = $localize`Save and close` | ||||
|     modal.componentInstance.alternativeClicked.pipe(first()).subscribe(() => { | ||||
|       modal.componentInstance.buttonsEnabled = false | ||||
|       component.saveViewConfig() | ||||
|       modal.close() | ||||
|     }) | ||||
|     modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => { | ||||
|       modal.componentInstance.buttonsEnabled = false | ||||
|       modal.close() | ||||
|     }) | ||||
|  | ||||
|     const subject = new Subject<boolean>() | ||||
|     modal.componentInstance.confirmSubject = subject | ||||
|     modal.componentInstance.alternativeSubject = subject | ||||
|  | ||||
|     return subject | ||||
|   } | ||||
| } | ||||
| @@ -171,15 +171,15 @@ export class DocumentListViewService { | ||||
|     this.reduceSelectionToFilter() | ||||
|  | ||||
|     if (!this.router.routerState.snapshot.url.includes('/view/')) { | ||||
|       this.router.navigate([], { | ||||
|         queryParams: { view: view.id }, | ||||
|       }) | ||||
|       this.router.navigate(['view', view.id]) | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   loadFromQueryParams(queryParams: ParamMap) { | ||||
|     const paramsEmpty: boolean = queryParams.keys.length == 0 | ||||
|     let newState: ListViewState = this.listViewStates.get(null) | ||||
|     let newState: ListViewState = this.listViewStates.get( | ||||
|       this._activeSavedViewId | ||||
|     ) | ||||
|     if (!paramsEmpty) newState = paramsToViewState(queryParams) | ||||
|     if (newState == undefined) newState = this.defaultListViewState() // if nothing in local storage | ||||
|  | ||||
| @@ -276,7 +276,6 @@ export class DocumentListViewService { | ||||
|     ) { | ||||
|       this.activeListViewState.sortField = 'created' | ||||
|     } | ||||
|     this._activeSavedViewId = null | ||||
|     this.activeListViewState.filterRules = filterRules | ||||
|     this.reload() | ||||
|     this.reduceSelectionToFilter() | ||||
| @@ -288,7 +287,6 @@ export class DocumentListViewService { | ||||
|   } | ||||
|  | ||||
|   set sortField(field: string) { | ||||
|     this._activeSavedViewId = null | ||||
|     this.activeListViewState.sortField = field | ||||
|     this.reload() | ||||
|     this.saveDocumentListView() | ||||
| @@ -299,7 +297,6 @@ export class DocumentListViewService { | ||||
|   } | ||||
|  | ||||
|   set sortReverse(reverse: boolean) { | ||||
|     this._activeSavedViewId = null | ||||
|     this.activeListViewState.sortReverse = reverse | ||||
|     this.reload() | ||||
|     this.saveDocumentListView() | ||||
|   | ||||
| @@ -397,6 +397,10 @@ textarea, | ||||
|       background-color: var(--bs-primary); | ||||
|       color: var(--pngx-primary-text-contrast); | ||||
|     } | ||||
|  | ||||
|     &.disabled, &:disabled { | ||||
|       opacity: 50%; | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 shamoon
					shamoon