diff --git a/src-ui/messages.xlf b/src-ui/messages.xlf
index 08e08608d..c79413a4b 100644
--- a/src-ui/messages.xlf
+++ b/src-ui/messages.xlf
@@ -179,21 +179,21 @@
Decrement hours
node_modules/src/timepicker/timepicker.ts
- 240,243
+ 239,240
Increment minutes
node_modules/src/timepicker/timepicker.ts
- 268
+ 264,268
Decrement minutes
node_modules/src/timepicker/timepicker.ts
- 288,289
+ 287,289
@@ -1648,7 +1648,7 @@
Confirm delete
src/app/components/document-detail/document-detail.component.ts
- 439
+ 442
src/app/components/manage/management-list/management-list.component.ts
@@ -1659,35 +1659,35 @@
Do you really want to delete document ""?
src/app/components/document-detail/document-detail.component.ts
- 440
+ 443
The files for this document will be deleted permanently. This operation cannot be undone.
src/app/components/document-detail/document-detail.component.ts
- 441
+ 444
Delete document
src/app/components/document-detail/document-detail.component.ts
- 443
+ 446
Error deleting document:
src/app/components/document-detail/document-detail.component.ts
- 459
+ 462
Redo OCR confirm
src/app/components/document-detail/document-detail.component.ts
- 479
+ 482
src/app/components/document-list/bulk-editor/bulk-editor.component.ts
@@ -1698,14 +1698,14 @@
This operation will permanently redo OCR for this document.
src/app/components/document-detail/document-detail.component.ts
- 480
+ 483
This operation cannot be undone.
src/app/components/document-detail/document-detail.component.ts
- 481
+ 484
src/app/components/document-list/bulk-editor/bulk-editor.component.ts
@@ -1720,7 +1720,7 @@
Proceed
src/app/components/document-detail/document-detail.component.ts
- 483
+ 486
src/app/components/document-list/bulk-editor/bulk-editor.component.ts
@@ -1731,7 +1731,7 @@
Redo OCR operation will begin in the background.
src/app/components/document-detail/document-detail.component.ts
- 491
+ 494
@@ -1740,7 +1740,7 @@
)"/>
src/app/components/document-detail/document-detail.component.ts
- 502,504
+ 505,507
@@ -3200,6 +3200,13 @@
39
+
+ Warning: You have unsaved changes to your document(s).
+
+ src/app/guards/dirty-doc.guard.ts
+ 18
+
+
Unsaved Changes
@@ -3208,11 +3215,11 @@
src/app/services/open-documents.service.ts
- 111
+ 116
src/app/services/open-documents.service.ts
- 138
+ 143
@@ -3223,7 +3230,7 @@
src/app/services/open-documents.service.ts
- 139
+ 144
@@ -3360,35 +3367,35 @@
You have unsaved changes to the document
src/app/services/open-documents.service.ts
- 113
+ 118
Are you sure you want to close this document?
src/app/services/open-documents.service.ts
- 117
+ 122
Close document
src/app/services/open-documents.service.ts
- 119
+ 124
Are you sure you want to close all documents?
src/app/services/open-documents.service.ts
- 140
+ 145
Close documents
src/app/services/open-documents.service.ts
- 142
+ 147
diff --git a/src-ui/src/app/app-routing.module.ts b/src-ui/src/app/app-routing.module.ts
index 65db5f97e..c62357c5d 100644
--- a/src-ui/src/app/app-routing.module.ts
+++ b/src-ui/src/app/app-routing.module.ts
@@ -14,12 +14,14 @@ import { DocumentAsnComponent } from './components/document-asn/document-asn.com
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'
const routes: Routes = [
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
{
path: '',
component: AppFrameComponent,
+ canDeactivate: [DirtyDocGuard],
children: [
{ path: 'dashboard', component: DashboardComponent },
{ path: 'documents', component: DocumentListComponent },
diff --git a/src-ui/src/app/app.module.ts b/src-ui/src/app/app.module.ts
index f4c49d95d..edbd261f6 100644
--- a/src-ui/src/app/app.module.ts
+++ b/src-ui/src/app/app.module.ts
@@ -67,6 +67,7 @@ import { ApiVersionInterceptor } from './interceptors/api-version.interceptor'
import { ColorSliderModule } from 'ngx-color/slider'
import { ColorComponent } from './components/common/input/color/color.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
+import { DirtyDocGuard } from './guards/dirty-doc.guard'
import localeBe from '@angular/common/locales/be'
import localeCs from '@angular/common/locales/cs'
@@ -209,6 +210,7 @@ function initializeApp(settings: SettingsService) {
DocumentTitlePipe,
{ provide: NgbDateAdapter, useClass: ISODateAdapter },
{ provide: NgbDateParserFormatter, useClass: LocalizedDateParserFormatter },
+ DirtyDocGuard,
],
bootstrap: [AppComponent],
})
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 441a75330..6e758d262 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,6 +1,6 @@
-import { Component } from '@angular/core'
+import { Component, HostListener } from '@angular/core'
import { FormControl } from '@angular/forms'
-import { ActivatedRoute, Router, Params } from '@angular/router'
+import { ActivatedRoute, Router } from '@angular/router'
import { from, Observable } from 'rxjs'
import {
debounceTime,
@@ -23,13 +23,14 @@ import {
} from 'src/app/services/rest/remote-version.service'
import { SettingsService } from 'src/app/services/settings.service'
import { TasksService } from 'src/app/services/tasks.service'
+import { ComponentCanDeactivate } from 'src/app/guards/dirty-doc.guard'
@Component({
selector: 'app-app-frame',
templateUrl: './app-frame.component.html',
styleUrls: ['./app-frame.component.scss'],
})
-export class AppFrameComponent {
+export class AppFrameComponent implements ComponentCanDeactivate {
constructor(
public router: Router,
private activatedRoute: ActivatedRoute,
@@ -64,6 +65,11 @@ export class AppFrameComponent {
return this.openDocumentsService.getOpenDocuments()
}
+ @HostListener('window:beforeunload')
+ canDeactivate(): Observable | boolean {
+ return !this.openDocumentsService.hasDirty()
+ }
+
searchAutoComplete = (text$: Observable) =>
text$.pipe(
debounceTime(200),
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 f86ee1ea8..203a56f04 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
@@ -206,7 +206,7 @@ export class DocumentDetailComponent
this.store.getValue().title !==
this.documentForm.get('title').value
) {
- this.openDocumentService.setDirty(doc.id, true)
+ this.openDocumentService.setDirty(doc, true)
}
},
})
@@ -228,12 +228,15 @@ export class DocumentDetailComponent
this.store.asObservable()
)
- return this.isDirty$.pipe(map((dirty) => ({ doc, dirty })))
+ return this.isDirty$.pipe(
+ takeUntil(this.unsubscribeNotifier),
+ map((dirty) => ({ doc, dirty }))
+ )
})
)
.subscribe({
next: ({ doc, dirty }) => {
- this.openDocumentService.setDirty(doc.id, dirty)
+ this.openDocumentService.setDirty(doc, dirty)
},
error: (error) => {
this.router.navigate(['404'])
@@ -349,7 +352,7 @@ export class DocumentDetailComponent
Object.assign(this.document, doc)
this.title = doc.title
this.documentForm.patchValue(doc)
- this.openDocumentService.setDirty(doc.id, false)
+ this.openDocumentService.setDirty(doc, false)
},
error: () => {
this.router.navigate(['404'])
diff --git a/src-ui/src/app/guards/dirty-doc.guard.ts b/src-ui/src/app/guards/dirty-doc.guard.ts
new file mode 100644
index 000000000..10362db45
--- /dev/null
+++ b/src-ui/src/app/guards/dirty-doc.guard.ts
@@ -0,0 +1,20 @@
+import { CanDeactivate } from '@angular/router'
+import { Injectable } from '@angular/core'
+import { Observable } from 'rxjs'
+
+export interface ComponentCanDeactivate {
+ canDeactivate: () => boolean | Observable
+}
+
+@Injectable()
+export class DirtyDocGuard implements CanDeactivate {
+ canDeactivate(
+ component: ComponentCanDeactivate
+ ): boolean | Observable {
+ return component.canDeactivate()
+ ? true
+ : confirm(
+ $localize`Warning: You have unsaved changes to your document(s).`
+ )
+ }
+}
diff --git a/src-ui/src/app/services/open-documents.service.ts b/src-ui/src/app/services/open-documents.service.ts
index d7746d261..8533166c3 100644
--- a/src-ui/src/app/services/open-documents.service.ts
+++ b/src-ui/src/app/services/open-documents.service.ts
@@ -92,9 +92,14 @@ export class OpenDocumentsService {
}
}
- setDirty(documentId: number, dirty: boolean) {
- if (dirty) this.dirtyDocuments.add(documentId)
- else this.dirtyDocuments.delete(documentId)
+ setDirty(doc: PaperlessDocument, dirty: boolean) {
+ if (!this.openDocuments.find((d) => d.id == doc.id)) return
+ if (dirty) this.dirtyDocuments.add(doc.id)
+ else this.dirtyDocuments.delete(doc.id)
+ }
+
+ hasDirty(): boolean {
+ return this.dirtyDocuments.size > 0
}
closeDocument(doc: PaperlessDocument): Observable {