mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-02-17 09:03:57 +00:00
Frontend handle this
This commit is contained in:
@@ -95,6 +95,21 @@
|
||||
<div class="col-md-6 col-xl-5 mb-4">
|
||||
|
||||
<form [formGroup]='documentForm' (ngSubmit)="save()">
|
||||
@if (remoteUpdateDetected) {
|
||||
<div class="alert alert-warning d-flex flex-column flex-md-row align-items-md-center gap-2" role="alert">
|
||||
<div class="flex-fill">
|
||||
<div class="fw-semibold" i18n>Document was updated.</div>
|
||||
@if (remoteUpdateModified) {
|
||||
<div class="small" i18n>Remote update detected at {{ remoteUpdateModified | date:'medium' }}.</div>
|
||||
}
|
||||
<div class="small" i18n>Saving your local edits now may overwrite remote changes.</div>
|
||||
</div>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button type="button" class="btn btn-warning" (click)="reloadRemoteVersion()" i18n>Reload</button>
|
||||
<button type="button" class="btn btn-outline-secondary" (click)="dismissRemoteUpdateWarning()" i18n>Dismiss</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="btn-toolbar mb-1 border-bottom">
|
||||
<div class="btn-group pb-3">
|
||||
|
||||
@@ -1205,17 +1205,19 @@ describe('DocumentDetailComponent', () => {
|
||||
expect(errorSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should warn when open document does not match doc retrieved from backend on init', () => {
|
||||
let openModal: NgbModalRef
|
||||
modalService.activeInstances.subscribe((modals) => (openModal = modals[0]))
|
||||
it('should show remote update warning when open local draft is older than backend on init', () => {
|
||||
const modalSpy = jest.spyOn(modalService, 'open')
|
||||
const openDoc = Object.assign({}, doc)
|
||||
const openDoc = Object.assign({}, doc, {
|
||||
__changedFields: ['title'],
|
||||
})
|
||||
// simulate a document being modified elsewhere and db updated
|
||||
doc.modified = new Date()
|
||||
const remoteDoc = Object.assign({}, doc, {
|
||||
modified: new Date(new Date(doc.modified).getTime() + 1000),
|
||||
})
|
||||
jest
|
||||
.spyOn(activatedRoute, 'paramMap', 'get')
|
||||
.mockReturnValue(of(convertToParamMap({ id: 3, section: 'details' })))
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
|
||||
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(remoteDoc))
|
||||
jest.spyOn(openDocumentsService, 'getOpenDocument').mockReturnValue(openDoc)
|
||||
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
|
||||
of({
|
||||
@@ -1225,11 +1227,11 @@ describe('DocumentDetailComponent', () => {
|
||||
})
|
||||
)
|
||||
fixture.detectChanges() // calls ngOnInit
|
||||
expect(modalSpy).toHaveBeenCalledWith(ConfirmDialogComponent)
|
||||
const closeSpy = jest.spyOn(openModal, 'close')
|
||||
const confirmDialog = openModal.componentInstance as ConfirmDialogComponent
|
||||
confirmDialog.confirmClicked.next(confirmDialog)
|
||||
expect(closeSpy).toHaveBeenCalled()
|
||||
expect(component.remoteUpdateDetected).toBeTruthy()
|
||||
expect(component.remoteUpdateModified).toEqual(
|
||||
remoteDoc.modified.toISOString()
|
||||
)
|
||||
expect(modalSpy).not.toHaveBeenCalledWith(ConfirmDialogComponent)
|
||||
})
|
||||
|
||||
it('should change preview element by render type', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
|
||||
import { AsyncPipe, DatePipe, NgTemplateOutlet } from '@angular/common'
|
||||
import { HttpClient, HttpResponse } from '@angular/common/http'
|
||||
import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core'
|
||||
import {
|
||||
@@ -80,6 +80,7 @@ import { TagService } from 'src/app/services/rest/tag.service'
|
||||
import { UserService } from 'src/app/services/rest/user.service'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
import { ToastService } from 'src/app/services/toast.service'
|
||||
import { WebsocketStatusService } from 'src/app/services/websocket-status.service'
|
||||
import { getFilenameFromContentDisposition } from 'src/app/utils/http'
|
||||
import { ISODateAdapter } from 'src/app/utils/ngb-iso-date-adapter'
|
||||
import * as UTIF from 'utif'
|
||||
@@ -163,6 +164,7 @@ enum ContentRenderType {
|
||||
MonetaryComponent,
|
||||
UrlComponent,
|
||||
SuggestionsDropdownComponent,
|
||||
DatePipe,
|
||||
CustomDatePipe,
|
||||
FileSizePipe,
|
||||
IfPermissionsDirective,
|
||||
@@ -205,6 +207,7 @@ export class DocumentDetailComponent
|
||||
private componentRouterService = inject(ComponentRouterService)
|
||||
private deviceDetectorService = inject(DeviceDetectorService)
|
||||
private savedViewService = inject(SavedViewService)
|
||||
private websocketStatusService = inject(WebsocketStatusService)
|
||||
|
||||
@ViewChild('inputTitle')
|
||||
titleInput: TextComponent
|
||||
@@ -270,6 +273,8 @@ export class DocumentDetailComponent
|
||||
customFields: CustomField[]
|
||||
|
||||
public downloading: boolean = false
|
||||
remoteUpdateDetected: boolean = false
|
||||
remoteUpdateModified: string | null = null
|
||||
|
||||
public readonly CustomFieldDataType = CustomFieldDataType
|
||||
|
||||
@@ -432,7 +437,14 @@ export class DocumentDetailComponent
|
||||
)
|
||||
}
|
||||
|
||||
private loadDocument(documentId: number): void {
|
||||
private hasLocalEdits(doc: Document): boolean {
|
||||
return (
|
||||
this.openDocumentService.isDirty(doc) || !!doc.__changedFields?.length
|
||||
)
|
||||
}
|
||||
|
||||
private loadDocument(documentId: number, forceRemote: boolean = false): void {
|
||||
this.dismissRemoteUpdateWarning()
|
||||
this.previewUrl = this.documentsService.getPreviewUrl(documentId)
|
||||
this.updatePdfSource()
|
||||
this.http
|
||||
@@ -477,21 +489,28 @@ export class DocumentDetailComponent
|
||||
openDocument.duplicate_documents = doc.duplicate_documents
|
||||
this.openDocumentService.save()
|
||||
}
|
||||
const useDoc = openDocument || doc
|
||||
if (openDocument) {
|
||||
if (
|
||||
new Date(doc.modified) > new Date(openDocument.modified) &&
|
||||
!this.modalService.hasOpenModals()
|
||||
) {
|
||||
const modal = this.modalService.open(ConfirmDialogComponent)
|
||||
modal.componentInstance.title = $localize`Document changes detected`
|
||||
modal.componentInstance.messageBold = $localize`The version of this document in your browser session appears older than the existing version.`
|
||||
modal.componentInstance.message = $localize`Saving the document here may overwrite other changes that were made. To restore the existing version, discard your changes or close the document.`
|
||||
modal.componentInstance.cancelBtnClass = 'visually-hidden'
|
||||
modal.componentInstance.btnCaption = $localize`Ok`
|
||||
modal.componentInstance.confirmClicked.subscribe(() =>
|
||||
modal.close()
|
||||
)
|
||||
let useDoc = openDocument || doc
|
||||
if (openDocument && forceRemote) {
|
||||
Object.assign(openDocument, doc)
|
||||
openDocument.__changedFields = []
|
||||
this.openDocumentService.setDirty(openDocument, false)
|
||||
this.openDocumentService.save()
|
||||
useDoc = openDocument
|
||||
} else if (openDocument) {
|
||||
if (new Date(doc.modified) > new Date(openDocument.modified)) {
|
||||
if (this.hasLocalEdits(openDocument)) {
|
||||
this.remoteUpdateDetected = true
|
||||
this.remoteUpdateModified = doc.modified
|
||||
? new Date(doc.modified).toISOString()
|
||||
: null
|
||||
} else {
|
||||
// No local edits to preserve, so keep the tab in sync automatically.
|
||||
Object.assign(openDocument, doc)
|
||||
openDocument.__changedFields = []
|
||||
this.openDocumentService.setDirty(openDocument, false)
|
||||
this.openDocumentService.save()
|
||||
useDoc = openDocument
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.openDocumentService
|
||||
@@ -522,6 +541,38 @@ export class DocumentDetailComponent
|
||||
})
|
||||
}
|
||||
|
||||
private handleIncomingDocumentUpdated(data: {
|
||||
document_id: number
|
||||
modified?: string
|
||||
}): void {
|
||||
if (!this.documentId || data.document_id !== this.documentId) return
|
||||
if (!this.document || this.networkActive) return
|
||||
|
||||
if (this.openDocumentService.isDirty(this.document)) {
|
||||
this.remoteUpdateDetected = true
|
||||
this.remoteUpdateModified = data.modified ?? null
|
||||
} else {
|
||||
this.docChangeNotifier.next(this.documentId)
|
||||
this.loadDocument(this.documentId, true)
|
||||
this.toastService.showInfo(
|
||||
$localize`Document reloaded with latest changes.`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dismissRemoteUpdateWarning() {
|
||||
this.remoteUpdateDetected = false
|
||||
this.remoteUpdateModified = null
|
||||
}
|
||||
|
||||
reloadRemoteVersion() {
|
||||
if (!this.documentId) return
|
||||
|
||||
this.docChangeNotifier.next(this.documentId)
|
||||
this.loadDocument(this.documentId, true)
|
||||
this.toastService.showInfo($localize`Document reloaded.`)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.setZoom(
|
||||
this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING) as PdfZoomScale
|
||||
@@ -580,6 +631,11 @@ export class DocumentDetailComponent
|
||||
|
||||
this.getCustomFields()
|
||||
|
||||
this.websocketStatusService
|
||||
.onDocumentUpdated()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((data) => this.handleIncomingDocumentUpdated(data))
|
||||
|
||||
this.route.paramMap
|
||||
.pipe(
|
||||
filter(
|
||||
@@ -914,6 +970,7 @@ export class DocumentDetailComponent
|
||||
)
|
||||
.subscribe({
|
||||
next: (doc) => {
|
||||
this.dismissRemoteUpdateWarning()
|
||||
Object.assign(this.document, doc)
|
||||
doc['permissions_form'] = {
|
||||
owner: doc.owner,
|
||||
@@ -960,6 +1017,7 @@ export class DocumentDetailComponent
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: (docValues) => {
|
||||
this.dismissRemoteUpdateWarning()
|
||||
// in case data changed while saving eg removing inbox_tags
|
||||
this.documentForm.patchValue(docValues)
|
||||
const newValues = Object.assign({}, this.documentForm.value)
|
||||
@@ -1039,6 +1097,7 @@ export class DocumentDetailComponent
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: ({ updateResult, nextDocId, closeResult }) => {
|
||||
this.dismissRemoteUpdateWarning()
|
||||
this.error = null
|
||||
this.networkActive = false
|
||||
if (closeResult && updateResult && nextDocId) {
|
||||
@@ -1135,7 +1194,7 @@ export class DocumentDetailComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo(
|
||||
$localize`Reprocess operation for "${this.document.title}" will begin in the background. Close and re-open or reload this document after the operation has completed to see new content.`
|
||||
$localize`Reprocess operation for "${this.document.title}" will begin in the background.`
|
||||
)
|
||||
if (modal) {
|
||||
modal.close()
|
||||
|
||||
Reference in New Issue
Block a user