diff --git a/src-ui/package-lock.json b/src-ui/package-lock.json
index d17acc50e..e470a82b8 100644
--- a/src-ui/package-lock.json
+++ b/src-ui/package-lock.json
@@ -19,6 +19,7 @@
"@angular/router": "~11.2.14",
"@ng-bootstrap/ng-bootstrap": "^9.1.2",
"@ng-select/ng-select": "^7.0.0",
+ "@ngneat/dirty-check-forms": "^1.1.0",
"bootstrap": "^4.5.0",
"file-saver": "^2.0.5",
"ng2-pdf-viewer": "^6.3.2",
@@ -2427,6 +2428,14 @@
"@angular/forms": ">=11.0.0 <12.0.0"
}
},
+ "node_modules/@ngneat/dirty-check-forms": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@ngneat/dirty-check-forms/-/dirty-check-forms-1.1.0.tgz",
+ "integrity": "sha512-Ak6SUMUV2oFlaylhUnar1yT4ahmq3Y2mHrd9uQHesE0iUZWfQTrIN07kMtwyT2JXR/x4RqdAmvp/+IJ+QlUPGg==",
+ "peerDependencies": {
+ "tslib": "^1.10.0"
+ }
+ },
"node_modules/@ngtools/webpack": {
"version": "11.2.14",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-11.2.14.tgz",
@@ -18869,6 +18878,12 @@
"tslib": "^2.0.0"
}
},
+ "@ngneat/dirty-check-forms": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@ngneat/dirty-check-forms/-/dirty-check-forms-1.1.0.tgz",
+ "integrity": "sha512-Ak6SUMUV2oFlaylhUnar1yT4ahmq3Y2mHrd9uQHesE0iUZWfQTrIN07kMtwyT2JXR/x4RqdAmvp/+IJ+QlUPGg==",
+ "requires": {}
+ },
"@ngtools/webpack": {
"version": "11.2.14",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-11.2.14.tgz",
diff --git a/src-ui/package.json b/src-ui/package.json
index b85368559..cd0d3f53b 100644
--- a/src-ui/package.json
+++ b/src-ui/package.json
@@ -22,6 +22,7 @@
"@angular/router": "~11.2.14",
"@ng-bootstrap/ng-bootstrap": "^9.1.2",
"@ng-select/ng-select": "^7.0.0",
+ "@ngneat/dirty-check-forms": "^1.1.0",
"bootstrap": "^4.5.0",
"file-saver": "^2.0.5",
"ng2-pdf-viewer": "^6.3.2",
diff --git a/src-ui/src/app/app-routing.module.ts b/src-ui/src/app/app-routing.module.ts
index 031a4bb0b..c2ea53bb7 100644
--- a/src-ui/src/app/app-routing.module.ts
+++ b/src-ui/src/app/app-routing.module.ts
@@ -11,6 +11,7 @@ import { SettingsComponent } from './components/manage/settings/settings.compone
import { TagListComponent } from './components/manage/tag-list/tag-list.component';
import { NotFoundComponent } from './components/not-found/not-found.component';
import {DocumentAsnComponent} from "./components/document-asn/document-asn.component";
+import { DirtyFormGuard } from './guards/dirty-form.guard';
const routes: Routes = [
{path: '', redirectTo: 'dashboard', pathMatch: 'full'},
@@ -19,13 +20,12 @@ const routes: Routes = [
{path: 'documents', component: DocumentListComponent },
{path: 'view/:id', component: DocumentListComponent },
{path: 'documents/:id', component: DocumentDetailComponent },
- {path: 'asn/:id', component: DocumentAsnComponent },
-
+ {path: 'asn/:id', component: DocumentAsnComponent },
{path: 'tags', component: TagListComponent },
{path: 'documenttypes', component: DocumentTypeListComponent },
{path: 'correspondents', component: CorrespondentListComponent },
{path: 'logs', component: LogsComponent },
- {path: 'settings', component: SettingsComponent },
+ {path: 'settings', component: SettingsComponent, canDeactivate: [DirtyFormGuard] },
]},
{path: '404', component: NotFoundComponent},
diff --git a/src-ui/src/app/components/app-frame/app-frame.component.ts b/src-ui/src/app/components/app-frame/app-frame.component.ts
index f8e76f0ae..74f3cd5b5 100644
--- a/src-ui/src/app/components/app-frame/app-frame.component.ts
+++ b/src-ui/src/app/components/app-frame/app-frame.component.ts
@@ -1,8 +1,8 @@
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';
import { ActivatedRoute, Router, Params } from '@angular/router';
import { from, Observable, Subscription, BehaviorSubject } from 'rxjs';
-import { debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
+import { debounceTime, distinctUntilChanged, map, switchMap, first } from 'rxjs/operators';
import { PaperlessDocument } from 'src/app/data/paperless-document';
import { OpenDocumentsService } from 'src/app/services/open-documents.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
@@ -18,7 +18,7 @@ import { FILTER_FULLTEXT_QUERY } from 'src/app/data/filter-rule-type';
templateUrl: './app-frame.component.html',
styleUrls: ['./app-frame.component.scss']
})
-export class AppFrameComponent implements OnInit {
+export class AppFrameComponent {
constructor (
public router: Router,
@@ -28,9 +28,7 @@ export class AppFrameComponent implements OnInit {
public savedViewService: SavedViewService,
private list: DocumentListViewService,
private meta: Meta
- ) {
-
- }
+ ) { }
versionString = `${environment.appTitle} ${environment.version}`
@@ -81,32 +79,36 @@ export class AppFrameComponent implements OnInit {
}
closeDocument(d: PaperlessDocument) {
- this.closeMenu()
- this.openDocumentsService.closeDocument(d)
-
- let route = this.activatedRoute.snapshot
- while (route.firstChild) {
- route = route.firstChild
- }
- if (route.component == DocumentDetailComponent && route.params['id'] == d.id) {
- this.router.navigate([""])
- }
+ this.openDocumentsService.closeDocument(d).pipe(first()).subscribe(confirmed => {
+ if (confirmed) {
+ this.closeMenu()
+ let route = this.activatedRoute.snapshot
+ while (route.firstChild) {
+ route = route.firstChild
+ }
+ if (route.component == DocumentDetailComponent && route.params['id'] == d.id) {
+ this.router.navigate([""])
+ }
+ }
+ })
}
closeAll() {
- this.closeMenu()
- this.openDocumentsService.closeAll()
+ // user may need to confirm losing unsaved changes
+ this.openDocumentsService.closeAll().pipe(first()).subscribe(confirmed => {
+ if (confirmed) {
+ this.closeMenu()
- let route = this.activatedRoute.snapshot
- while (route.firstChild) {
- route = route.firstChild
- }
- if (route.component == DocumentDetailComponent) {
- this.router.navigate([""])
- }
- }
-
- ngOnInit() {
+ // TODO: is there a better way to do this?
+ let route = this.activatedRoute
+ while (route.firstChild) {
+ route = route.firstChild
+ }
+ if (route.component === DocumentDetailComponent) {
+ this.router.navigate([""])
+ }
+ }
+ })
}
get displayName() {
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 8a8a297d9..780ae6891 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
@@ -1,6 +1,6 @@
@@ -9,8 +9,8 @@
{{message}}
@@ -154,6 +162,5 @@
-
diff --git a/src-ui/src/app/components/document-detail/document-detail.component.ts b/src-ui/src/app/components/document-detail/document-detail.component.ts
index e711b3416..85b221307 100644
--- a/src-ui/src/app/components/document-detail/document-detail.component.ts
+++ b/src-ui/src/app/components/document-detail/document-detail.component.ts
@@ -1,4 +1,4 @@
-import { Component, OnInit, ViewChild, ElementRef } from '@angular/core';
+import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { NgbModal, NgbNav } from '@ng-bootstrap/ng-bootstrap';
@@ -19,6 +19,9 @@ import { PDFDocumentProxy } from 'ng2-pdf-viewer';
import { ToastService } from 'src/app/services/toast.service';
import { TextComponent } from '../common/input/text/text.component';
import { SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
+import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms';
+import { Observable, Subject, BehaviorSubject } from 'rxjs';
+import { first, takeUntil, switchMap, map, debounceTime, distinctUntilChanged } from 'rxjs/operators';
import { PaperlessDocumentSuggestions } from 'src/app/data/paperless-document-suggestions';
import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type';
@@ -27,7 +30,7 @@ import { FILTER_FULLTEXT_MORELIKE } from 'src/app/data/filter-rule-type';
templateUrl: './document-detail.component.html',
styleUrls: ['./document-detail.component.scss']
})
-export class DocumentDetailComponent implements OnInit {
+export class DocumentDetailComponent implements OnInit, OnDestroy, DirtyComponent {
@ViewChild("inputTitle")
titleInput: TextComponent
@@ -45,6 +48,7 @@ export class DocumentDetailComponent implements OnInit {
suggestions: PaperlessDocumentSuggestions
title: string
+ titleSubject: Subject = new Subject()
previewUrl: string
downloadUrl: string
downloadOriginalUrl: string
@@ -65,11 +69,14 @@ export class DocumentDetailComponent implements OnInit {
previewCurrentPage: number = 1
previewNumPages: number = 1
+ store: BehaviorSubject
+ isDirty$: Observable
+ unsubscribeNotifier: Subject = new Subject()
+
@ViewChild('nav') nav: NgbNav
@ViewChild('pdfPreview') set pdfPreview(element) {
// this gets called when compontent added or removed from DOM
- if (element && element.nativeElement.offsetParent !== null) { // its visible
-
+ if (element && element.nativeElement.offsetParent !== null && this.nav?.activeId == 4) { // its visible
setTimeout(()=> this.nav?.select(1));
}
}
@@ -85,7 +92,19 @@ export class DocumentDetailComponent implements OnInit {
private documentListViewService: DocumentListViewService,
private documentTitlePipe: DocumentTitlePipe,
private toastService: ToastService,
- private settings: SettingsService) { }
+ private settings: SettingsService) {
+ this.titleSubject.pipe(
+ debounceTime(200),
+ distinctUntilChanged(),
+ takeUntil(this.unsubscribeNotifier)
+ ).subscribe(titleValue => {
+ this.documentForm.patchValue({'title': titleValue})
+ })
+ }
+
+ titleKeyUp(event) {
+ this.titleSubject.next(event.target?.value)
+ }
get useNativePdfViewer(): boolean {
return this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)
@@ -96,15 +115,18 @@ export class DocumentDetailComponent implements OnInit {
}
ngOnInit(): void {
- this.documentForm.valueChanges.subscribe(wow => {
+ this.documentForm.valueChanges.pipe(takeUntil(this.unsubscribeNotifier)).subscribe(wow => {
Object.assign(this.document, this.documentForm.value)
})
- this.correspondentService.listAll().subscribe(result => this.correspondents = result.results)
- this.documentTypeService.listAll().subscribe(result => this.documentTypes = result.results)
+ this.correspondentService.listAll().pipe(first()).subscribe(result => this.correspondents = result.results)
+ this.documentTypeService.listAll().pipe(first()).subscribe(result => this.documentTypes = result.results)
- this.route.paramMap.subscribe(paramMap => {
- this.documentId = +paramMap.get('id')
+ this.route.paramMap.pipe(switchMap(paramMap => {
+ const documentId = +paramMap.get('id')
+ return this.documentsService.get(documentId)
+ })).pipe(switchMap((doc) => {
+ this.documentId = doc.id
this.previewUrl = this.documentsService.getPreviewUrl(this.documentId)
this.downloadUrl = this.documentsService.getDownloadUrl(this.documentId)
this.downloadOriginalUrl = this.documentsService.getDownloadUrl(this.documentId, true)
@@ -112,23 +134,44 @@ export class DocumentDetailComponent implements OnInit {
if (this.openDocumentService.getOpenDocument(this.documentId)) {
this.updateComponent(this.openDocumentService.getOpenDocument(this.documentId))
} else {
- this.documentsService.get(this.documentId).subscribe(doc => {
- this.openDocumentService.openDocument(doc)
- this.updateComponent(doc)
- }, error => {this.router.navigate(['404'])})
+ this.openDocumentService.openDocument(doc)
+ this.updateComponent(doc)
}
- })
+ // Initialize dirtyCheck
+ this.store = new BehaviorSubject({
+ title: doc.title,
+ content: doc.content,
+ created: doc.created,
+ correspondent: doc.correspondent,
+ document_type: doc.document_type,
+ archive_serial_number: doc.archive_serial_number,
+ tags: doc.tags
+ })
+
+ this.isDirty$ = dirtyCheck(this.documentForm, this.store.asObservable())
+
+ return this.isDirty$.pipe(map(dirty => ({doc, dirty})))
+ }))
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe(({doc, dirty}) => {
+ this.openDocumentService.setDirty(doc.id, dirty)
+ }, error => {this.router.navigate(['404'])})
+ }
+
+ ngOnDestroy() : void {
+ this.unsubscribeNotifier.next();
+ this.unsubscribeNotifier.complete();
}
updateComponent(doc: PaperlessDocument) {
this.document = doc
- this.documentsService.getMetadata(doc.id).subscribe(result => {
+ this.documentsService.getMetadata(doc.id).pipe(first()).subscribe(result => {
this.metadata = result
}, error => {
this.metadata = null
})
- this.documentsService.getSuggestions(doc.id).subscribe(result => {
+ this.documentsService.getSuggestions(doc.id).pipe(first()).subscribe(result => {
this.suggestions = result
}, error => {
this.suggestions = null
@@ -141,11 +184,13 @@ export class DocumentDetailComponent implements OnInit {
var modal = this.modalService.open(DocumentTypeEditDialogComponent, {backdrop: 'static'})
modal.componentInstance.dialogMode = 'create'
if (newName) modal.componentInstance.object = { name: newName }
- modal.componentInstance.success.subscribe(newDocumentType => {
- this.documentTypeService.listAll().subscribe(documentTypes => {
- this.documentTypes = documentTypes.results
- this.documentForm.get('document_type').setValue(newDocumentType.id)
- })
+ modal.componentInstance.success.pipe(switchMap(newDocumentType => {
+ return this.documentTypeService.listAll().pipe(map(documentTypes => ({newDocumentType, documentTypes})))
+ }))
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe(({newDocumentType, documentTypes}) => {
+ this.documentTypes = documentTypes.results
+ this.documentForm.get('document_type').setValue(newDocumentType.id)
})
}
@@ -153,16 +198,18 @@ export class DocumentDetailComponent implements OnInit {
var modal = this.modalService.open(CorrespondentEditDialogComponent, {backdrop: 'static'})
modal.componentInstance.dialogMode = 'create'
if (newName) modal.componentInstance.object = { name: newName }
- modal.componentInstance.success.subscribe(newCorrespondent => {
- this.correspondentService.listAll().subscribe(correspondents => {
- this.correspondents = correspondents.results
- this.documentForm.get('correspondent').setValue(newCorrespondent.id)
- })
+ modal.componentInstance.success.pipe(switchMap(newCorrespondent => {
+ return this.correspondentService.listAll().pipe(map(correspondents => ({newCorrespondent, correspondents})))
+ }))
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe(({newCorrespondent, correspondents}) => {
+ this.correspondents = correspondents.results
+ this.documentForm.get('correspondent').setValue(newCorrespondent.id)
})
}
discard() {
- this.documentsService.get(this.documentId).subscribe(doc => {
+ this.documentsService.get(this.documentId).pipe(first()).subscribe(doc => {
Object.assign(this.document, doc)
this.title = doc.title
this.documentForm.patchValue(doc)
@@ -171,7 +218,8 @@ export class DocumentDetailComponent implements OnInit {
save() {
this.networkActive = true
- this.documentsService.update(this.document).subscribe(result => {
+ this.store.next(this.documentForm.value)
+ this.documentsService.update(this.document).pipe(first()).subscribe(result => {
this.close()
this.networkActive = false
this.error = null
@@ -183,18 +231,20 @@ export class DocumentDetailComponent implements OnInit {
saveEditNext() {
this.networkActive = true
- this.documentsService.update(this.document).subscribe(result => {
+ this.store.next(this.documentForm.value)
+ this.documentsService.update(this.document).pipe(switchMap(updateResult => {
+ return this.documentListViewService.getNext(this.documentId).pipe(map(nextDocId => ({nextDocId, updateResult})))
+ })).pipe(switchMap(({nextDocId, updateResult}) => {
+ if (nextDocId && updateResult) return this.openDocumentService.closeDocument(this.document).pipe(map(closeResult => ({updateResult, nextDocId, closeResult})))
+ }))
+ .pipe(first())
+ .subscribe(({updateResult, nextDocId, closeResult}) => {
this.error = null
- this.documentListViewService.getNext(this.document.id).subscribe(nextDocId => {
- this.networkActive = false
- if (nextDocId) {
- this.openDocumentService.closeDocument(this.document)
- this.router.navigate(['documents', nextDocId])
- this.titleInput.focus()
- }
- }, error => {
- this.networkActive = false
- })
+ this.networkActive = false
+ if (closeResult && updateResult && nextDocId) {
+ this.router.navigate(['documents', nextDocId])
+ this.titleInput?.focus()
+ }
}, error => {
this.networkActive = false
this.error = error.error
@@ -202,12 +252,14 @@ export class DocumentDetailComponent implements OnInit {
}
close() {
- this.openDocumentService.closeDocument(this.document)
- if (this.documentListViewService.activeSavedViewId) {
- this.router.navigate(['view', this.documentListViewService.activeSavedViewId])
- } else {
- this.router.navigate(['documents'])
- }
+ this.openDocumentService.closeDocument(this.document).pipe(first()).subscribe(closed => {
+ if (!closed) return;
+ if (this.documentListViewService.activeSavedViewId) {
+ this.router.navigate(['view', this.documentListViewService.activeSavedViewId])
+ } else {
+ this.router.navigate(['documents'])
+ }
+ })
}
delete() {
@@ -217,17 +269,18 @@ export class DocumentDetailComponent implements OnInit {
modal.componentInstance.message = $localize`The files for this document will be deleted permanently. This operation cannot be undone.`
modal.componentInstance.btnClass = "btn-danger"
modal.componentInstance.btnCaption = $localize`Delete document`
- modal.componentInstance.confirmClicked.subscribe(() => {
+ modal.componentInstance.confirmClicked.pipe(switchMap(() => {
modal.componentInstance.buttonsEnabled = false
- this.documentsService.delete(this.document).subscribe(() => {
- modal.close()
- this.close()
- }, error => {
- this.toastService.showError($localize`Error deleting document: ${JSON.stringify(error)}`)
- modal.componentInstance.buttonsEnabled = true
- })
+ return this.documentsService.delete(this.document)
+ }))
+ .pipe(takeUntil(this.unsubscribeNotifier))
+ .subscribe(() => {
+ modal.close()
+ this.close()
+ }, error => {
+ this.toastService.showError($localize`Error deleting document: ${JSON.stringify(error)}`)
+ modal.componentInstance.buttonsEnabled = true
})
-
}
moreLike() {
diff --git a/src-ui/src/app/components/manage/settings/settings.component.html b/src-ui/src/app/components/manage/settings/settings.component.html
index c2a32906e..93a12faf4 100644
--- a/src-ui/src/app/components/manage/settings/settings.component.html
+++ b/src-ui/src/app/components/manage/settings/settings.component.html
@@ -170,5 +170,5 @@
-
+
diff --git a/src-ui/src/app/components/manage/settings/settings.component.ts b/src-ui/src/app/components/manage/settings/settings.component.ts
index ab71b1028..91dcc9809 100644
--- a/src-ui/src/app/components/manage/settings/settings.component.ts
+++ b/src-ui/src/app/components/manage/settings/settings.component.ts
@@ -1,40 +1,45 @@
-import { Component, Inject, LOCALE_ID, OnInit, Renderer2 } from '@angular/core';
+import { Component, Inject, LOCALE_ID, OnInit, OnDestroy, Renderer2 } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';
import { PaperlessSavedView } from 'src/app/data/paperless-saved-view';
import { DocumentListViewService } from 'src/app/services/document-list-view.service';
import { SavedViewService } from 'src/app/services/rest/saved-view.service';
import { LanguageOption, SettingsService, SETTINGS_KEYS } from 'src/app/services/settings.service';
import { ToastService } from 'src/app/services/toast.service';
+import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms';
+import { Observable, Subscription, BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-settings',
templateUrl: './settings.component.html',
styleUrls: ['./settings.component.scss']
})
-export class SettingsComponent implements OnInit {
+export class SettingsComponent implements OnInit, OnDestroy, DirtyComponent {
savedViewGroup = new FormGroup({})
settingsForm = new FormGroup({
- 'bulkEditConfirmationDialogs': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS)),
- 'bulkEditApplyOnClose': new FormControl(this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE)),
- 'documentListItemPerPage': new FormControl(this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE)),
- 'darkModeUseSystem': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM)),
- 'darkModeEnabled': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED)),
- 'darkModeInvertThumbs': new FormControl(this.settings.get(SETTINGS_KEYS.DARK_MODE_THUMB_INVERTED)),
- 'useNativePdfViewer': new FormControl(this.settings.get(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER)),
+ 'bulkEditConfirmationDialogs': new FormControl(null),
+ 'bulkEditApplyOnClose': new FormControl(null),
+ 'documentListItemPerPage': new FormControl(null),
+ 'darkModeUseSystem': new FormControl(null),
+ 'darkModeEnabled': new FormControl(null),
+ 'useNativePdfViewer': new FormControl(null),
'savedViews': this.savedViewGroup,
- 'displayLanguage': new FormControl(this.settings.getLanguage()),
- 'dateLocale': new FormControl(this.settings.get(SETTINGS_KEYS.DATE_LOCALE)),
- 'dateFormat': new FormControl(this.settings.get(SETTINGS_KEYS.DATE_FORMAT)),
- 'notificationsConsumerNewDocument': new FormControl(this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT)),
- 'notificationsConsumerSuccess': new FormControl(this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS)),
- 'notificationsConsumerFailed': new FormControl(this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED)),
- 'notificationsConsumerSuppressOnDashboard': new FormControl(this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD)),
+ 'displayLanguage': new FormControl(null),
+ 'dateLocale': new FormControl(null),
+ 'dateFormat': new FormControl(null),
+ 'notificationsConsumerNewDocument': new FormControl(null),
+ 'notificationsConsumerSuccess': new FormControl(null),
+ 'notificationsConsumerFailed': new FormControl(null),
+ 'notificationsConsumerSuppressOnDashboard': new FormControl(null),
})
savedViews: PaperlessSavedView[]
+ store: BehaviorSubject
+ storeSub: Subscription
+ isDirty$: Observable
+
get computedDateLocale(): string {
return this.settingsForm.value.dateLocale || this.settingsForm.value.displayLanguage || this.currentLocale
}
@@ -50,17 +55,53 @@ export class SettingsComponent implements OnInit {
ngOnInit() {
this.savedViewService.listAll().subscribe(r => {
this.savedViews = r.results
+ let storeData = {
+ 'bulkEditConfirmationDialogs': this.settings.get(SETTINGS_KEYS.BULK_EDIT_CONFIRMATION_DIALOGS),
+ 'bulkEditApplyOnClose': this.settings.get(SETTINGS_KEYS.BULK_EDIT_APPLY_ON_CLOSE),
+ 'documentListItemPerPage': this.settings.get(SETTINGS_KEYS.DOCUMENT_LIST_SIZE),
+ 'darkModeUseSystem': this.settings.get(SETTINGS_KEYS.DARK_MODE_USE_SYSTEM),
+ 'darkModeEnabled': this.settings.get(SETTINGS_KEYS.DARK_MODE_ENABLED),
+ '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),
+ 'notificationsConsumerNewDocument': this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_NEW_DOCUMENT),
+ 'notificationsConsumerSuccess': this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUCCESS),
+ 'notificationsConsumerFailed': this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED),
+ 'notificationsConsumerSuppressOnDashboard': this.settings.get(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD),
+ }
+
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(view.id),
- "name": new FormControl(view.name),
- "show_on_dashboard": new FormControl(view.show_on_dashboard),
- "show_in_sidebar": new FormControl(view.show_in_sidebar)
+ "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)
+
+ this.storeSub = this.store.asObservable().subscribe(state => {
+ this.settingsForm.patchValue(state, { emitEvent: false })
+ })
+
+ // Initialize dirtyCheck
+ this.isDirty$ = dirtyCheck(this.settingsForm, this.store.asObservable())
})
}
+ ngOnDestroy() {
+ this.storeSub && this.storeSub.unsubscribe();
+ }
+
deleteSavedView(savedView: PaperlessSavedView) {
this.savedViewService.delete(savedView).subscribe(() => {
this.savedViewGroup.removeControl(savedView.id.toString())
@@ -84,6 +125,7 @@ export class SettingsComponent implements OnInit {
this.settings.set(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_FAILED, this.settingsForm.value.notificationsConsumerFailed)
this.settings.set(SETTINGS_KEYS.NOTIFICATIONS_CONSUMER_SUPPRESS_ON_DASHBOARD, this.settingsForm.value.notificationsConsumerSuppressOnDashboard)
this.settings.setLanguage(this.settingsForm.value.displayLanguage)
+ this.store.next(this.settingsForm.value)
this.documentListViewService.updatePageSize()
this.settings.updateDarkModeSettings()
this.toastService.showInfo($localize`Settings saved successfully.`)
diff --git a/src-ui/src/app/guards/dirty-form.guard.ts b/src-ui/src/app/guards/dirty-form.guard.ts
new file mode 100644
index 000000000..4e7bc39ea
--- /dev/null
+++ b/src-ui/src/app/guards/dirty-form.guard.ts
@@ -0,0 +1,29 @@
+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';
+
+@Injectable({ providedIn: 'root' })
+export class DirtyFormGuard extends DirtyCheckGuard {
+ constructor(private modalService: NgbModal) {
+ super();
+ }
+
+ confirmChanges(): Observable {
+ let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
+ modal.componentInstance.title = $localize`Unsaved Changes`
+ modal.componentInstance.messageBold = $localize`You have unsaved changes.`
+ modal.componentInstance.message = $localize`Are you sure you want to leave?`
+ modal.componentInstance.btnClass = "btn-warning"
+ modal.componentInstance.btnCaption = $localize`Leave page`
+ modal.componentInstance.confirmClicked.subscribe(() => {
+ modal.componentInstance.buttonsEnabled = false
+ modal.close()
+ })
+ const subject = new Subject()
+ modal.componentInstance.confirmSubject = subject
+ return subject.asObservable()
+ }
+}
diff --git a/src-ui/src/app/services/open-documents.service.ts b/src-ui/src/app/services/open-documents.service.ts
index 01f168aeb..92802c765 100644
--- a/src-ui/src/app/services/open-documents.service.ts
+++ b/src-ui/src/app/services/open-documents.service.ts
@@ -2,6 +2,10 @@ import { Injectable } from '@angular/core';
import { PaperlessDocument } from '../data/paperless-document';
import { OPEN_DOCUMENT_SERVICE } from '../data/storage-keys';
import { DocumentService } from './rest/document.service';
+import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
+import { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component';
+import { Observable, Subject, of } from 'rxjs';
+import { first } from 'rxjs/operators';
@Injectable({
providedIn: 'root'
@@ -10,7 +14,7 @@ export class OpenDocumentsService {
private MAX_OPEN_DOCUMENTS = 5
- constructor(private documentService: DocumentService) {
+ constructor(private documentService: DocumentService, private modalService: NgbModal) {
if (sessionStorage.getItem(OPEN_DOCUMENT_SERVICE.DOCUMENTS)) {
try {
this.openDocuments = JSON.parse(sessionStorage.getItem(OPEN_DOCUMENT_SERVICE.DOCUMENTS))
@@ -22,6 +26,7 @@ export class OpenDocumentsService {
}
private openDocuments: PaperlessDocument[] = []
+ private dirtyDocuments: Set = new Set()
refreshDocument(id: number) {
let index = this.openDocuments.findIndex(doc => doc.id == id)
@@ -53,17 +58,62 @@ export class OpenDocumentsService {
}
}
- closeDocument(doc: PaperlessDocument) {
+ setDirty(documentId: number, dirty: boolean) {
+ if (dirty) this.dirtyDocuments.add(documentId)
+ else this.dirtyDocuments.delete(documentId)
+ }
+
+ closeDocument(doc: PaperlessDocument): Observable {
let index = this.openDocuments.findIndex(d => d.id == doc.id)
- if (index > -1) {
+ if (index == -1) return of(true);
+ if (!this.dirtyDocuments.has(doc.id)) {
this.openDocuments.splice(index, 1)
this.save()
+ return of(true)
+ } else {
+ let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
+ modal.componentInstance.title = $localize`Unsaved Changes`
+ modal.componentInstance.messageBold = $localize`You have unsaved changes.`
+ modal.componentInstance.message = $localize`Are you sure you want to close this document?`
+ modal.componentInstance.btnClass = "btn-warning"
+ modal.componentInstance.btnCaption = $localize`Close document`
+ modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
+ modal.componentInstance.buttonsEnabled = false
+ modal.close()
+ this.openDocuments.splice(index, 1)
+ this.dirtyDocuments.delete(doc.id)
+ this.save()
+ })
+ const subject = new Subject()
+ modal.componentInstance.confirmSubject = subject
+ return subject.asObservable()
}
}
- closeAll() {
- this.openDocuments.splice(0, this.openDocuments.length)
- this.save()
+ closeAll(): Observable {
+ if (this.dirtyDocuments.size) {
+ let modal = this.modalService.open(ConfirmDialogComponent, {backdrop: 'static'})
+ modal.componentInstance.title = $localize`Unsaved Changes`
+ modal.componentInstance.messageBold = $localize`You have unsaved changes.`
+ modal.componentInstance.message = $localize`Are you sure you want to close all documents?`
+ modal.componentInstance.btnClass = "btn-warning"
+ modal.componentInstance.btnCaption = $localize`Close documents`
+ modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
+ modal.componentInstance.buttonsEnabled = false
+ modal.close()
+ this.openDocuments.splice(0, this.openDocuments.length)
+ this.dirtyDocuments.clear()
+ this.save()
+ })
+ const subject = new Subject()
+ modal.componentInstance.confirmSubject = subject
+ return subject.asObservable()
+ } else {
+ this.openDocuments.splice(0, this.openDocuments.length)
+ this.dirtyDocuments.clear()
+ this.save()
+ return of(true)
+ }
}
save() {