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 @@
-
+
0">
-
+
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%;
+ }
}
}