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 {