diff --git a/src-ui/src/app/app-routing.module.ts b/src-ui/src/app/app-routing.module.ts index c62357c5d..4dad24c51 100644 --- a/src-ui/src/app/app-routing.module.ts +++ b/src-ui/src/app/app-routing.module.ts @@ -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 }, diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts index 02fd8ea66..29c85341b 100644 --- a/src-ui/src/app/app.module.ts +++ b/src-ui/src/app/app.module.ts @@ -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], }) diff --git a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.html b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.html index 92e27e370..290581a47 100644 --- a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.html +++ b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.html @@ -16,4 +16,7 @@ {{ seconds | number: '1.0-0' }} seconds + diff --git a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts index ddf0bfd7c..59d84bbe8 100644 --- a/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts +++ b/src-ui/src/app/components/common/confirm-dialog/confirm-dialog.component.ts @@ -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 + alternativeSubject: Subject 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() + } } diff --git a/src-ui/src/app/components/document-list/document-list.component.html b/src-ui/src/app/components/document-list/document-list.component.html index ca725b175..9357813f6 100644 --- a/src-ui/src/app/components/document-list/document-list.component.html +++ b/src-ui/src/app/components/document-list/document-list.component.html @@ -60,14 +60,19 @@
- +
diff --git a/src-ui/src/app/components/document-list/document-list.component.ts b/src-ui/src/app/components/document-list/document-list.component.ts index a0c6899f8..3c1e6775f 100644 --- a/src-ui/src/app/components/document-list/document-list.component.ts +++ b/src-ui/src/app/components/document-list/document-list.component.ts @@ -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 = 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() }) diff --git a/src-ui/src/app/guards/dirty-saved-view.guard.ts b/src-ui/src/app/guards/dirty-saved-view.guard.ts new file mode 100644 index 000000000..0044a2e78 --- /dev/null +++ b/src-ui/src/app/guards/dirty-saved-view.guard.ts @@ -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 +{ + constructor(private modalService: NgbModal) {} + + canDeactivate( + component: DocumentListComponent + ): boolean | Observable { + 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() + modal.componentInstance.confirmSubject = subject + modal.componentInstance.alternativeSubject = subject + + return subject + } +} diff --git a/src-ui/src/app/services/document-list-view.service.ts b/src-ui/src/app/services/document-list-view.service.ts index 1dea011c0..3eb036710 100644 --- a/src-ui/src/app/services/document-list-view.service.ts +++ b/src-ui/src/app/services/document-list-view.service.ts @@ -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() diff --git a/src-ui/src/styles.scss b/src-ui/src/styles.scss index 4efbf4937..0bde8364c 100644 --- a/src-ui/src/styles.scss +++ b/src-ui/src/styles.scss @@ -397,6 +397,10 @@ textarea, background-color: var(--bs-primary); color: var(--pngx-primary-text-contrast); } + + &.disabled, &:disabled { + opacity: 50%; + } } }