Actually use a modal

This commit is contained in:
shamoon
2026-02-17 09:45:59 -08:00
parent 0e9ca57d39
commit 56d92d849c
3 changed files with 60 additions and 42 deletions

View File

@@ -95,22 +95,6 @@
<div class="col-md-6 col-xl-5 mb-4"> <div class="col-md-6 col-xl-5 mb-4">
<form [formGroup]='documentForm' (ngSubmit)="save()"> <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-toolbar mb-1 border-bottom">
<div class="btn-group pb-3"> <div class="btn-group pb-3">
<button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()"> <button type="button" class="btn btn-sm btn-outline-secondary" i18n-title title="Close" (click)="close()">

View File

@@ -1205,7 +1205,9 @@ describe('DocumentDetailComponent', () => {
expect(errorSpy).toHaveBeenCalled() expect(errorSpy).toHaveBeenCalled()
}) })
it('should show remote update warning when open local draft is older than backend on init', () => { it('should show incoming update modal when open local draft is older than backend on init', () => {
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modals) => (openModal = modals[0]))
const modalSpy = jest.spyOn(modalService, 'open') const modalSpy = jest.spyOn(modalService, 'open')
const openDoc = Object.assign({}, doc, { const openDoc = Object.assign({}, doc, {
__changedFields: ['title'], __changedFields: ['title'],
@@ -1227,11 +1229,11 @@ describe('DocumentDetailComponent', () => {
}) })
) )
fixture.detectChanges() // calls ngOnInit fixture.detectChanges() // calls ngOnInit
expect(component.remoteUpdateDetected).toBeTruthy() expect(modalSpy).toHaveBeenCalledWith(ConfirmDialogComponent, {
expect(component.remoteUpdateModified).toEqual( backdrop: 'static',
remoteDoc.modified.toISOString() })
) const confirmDialog = openModal.componentInstance as ConfirmDialogComponent
expect(modalSpy).not.toHaveBeenCalledWith(ConfirmDialogComponent) expect(confirmDialog.messageBold).toContain('Remote update detected at')
}) })
it('should change preview element by render type', () => { it('should change preview element by render type', () => {

View File

@@ -1,4 +1,4 @@
import { AsyncPipe, DatePipe, NgTemplateOutlet } from '@angular/common' import { AsyncPipe, NgTemplateOutlet } from '@angular/common'
import { HttpClient, HttpResponse } from '@angular/common/http' import { HttpClient, HttpResponse } from '@angular/common/http'
import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core' import { Component, inject, OnDestroy, OnInit, ViewChild } from '@angular/core'
import { import {
@@ -13,6 +13,7 @@ import {
NgbDateStruct, NgbDateStruct,
NgbDropdownModule, NgbDropdownModule,
NgbModal, NgbModal,
NgbModalRef,
NgbNav, NgbNav,
NgbNavChangeEvent, NgbNavChangeEvent,
NgbNavModule, NgbNavModule,
@@ -164,7 +165,6 @@ enum ContentRenderType {
MonetaryComponent, MonetaryComponent,
UrlComponent, UrlComponent,
SuggestionsDropdownComponent, SuggestionsDropdownComponent,
DatePipe,
CustomDatePipe, CustomDatePipe,
FileSizePipe, FileSizePipe,
IfPermissionsDirective, IfPermissionsDirective,
@@ -264,6 +264,7 @@ export class DocumentDetailComponent
isDirty$: Observable<boolean> isDirty$: Observable<boolean>
unsubscribeNotifier: Subject<any> = new Subject() unsubscribeNotifier: Subject<any> = new Subject()
docChangeNotifier: Subject<any> = new Subject() docChangeNotifier: Subject<any> = new Subject()
private incomingUpdateModal: NgbModalRef
requiresPassword: boolean = false requiresPassword: boolean = false
password: string password: string
@@ -273,8 +274,6 @@ export class DocumentDetailComponent
customFields: CustomField[] customFields: CustomField[]
public downloading: boolean = false public downloading: boolean = false
remoteUpdateDetected: boolean = false
remoteUpdateModified: string | null = null
public readonly CustomFieldDataType = CustomFieldDataType public readonly CustomFieldDataType = CustomFieldDataType
@@ -443,8 +442,49 @@ export class DocumentDetailComponent
) )
} }
private showIncomingUpdateModal(modified?: string | Date): void {
if (this.incomingUpdateModal) return
const modal = this.modalService.open(ConfirmDialogComponent, {
backdrop: 'static',
})
this.incomingUpdateModal = modal
let formattedModified = null
if (modified) {
const parsed = new Date(modified)
if (!isNaN(parsed.getTime())) {
formattedModified = parsed.toLocaleString()
}
}
modal.componentInstance.title = $localize`Document was updated.`
modal.componentInstance.messageBold = formattedModified
? $localize`Document was updated at ${formattedModified}.`
: $localize`This document was updated elsewhere.`
modal.componentInstance.message = $localize`Reload to discard your local unsaved edits and load the latest remote version.`
modal.componentInstance.btnClass = 'btn-warning'
modal.componentInstance.btnCaption = $localize`Reload`
modal.componentInstance.cancelBtnCaption = $localize`Dismiss`
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
modal.componentInstance.buttonsEnabled = false
modal.close()
this.reloadRemoteVersion()
})
modal.result.finally(() => {
this.incomingUpdateModal = null
})
}
private closeIncomingUpdateModal() {
if (!this.incomingUpdateModal) return
this.incomingUpdateModal.close()
this.incomingUpdateModal = null
}
private loadDocument(documentId: number, forceRemote: boolean = false): void { private loadDocument(documentId: number, forceRemote: boolean = false): void {
this.dismissRemoteUpdateWarning() this.closeIncomingUpdateModal()
this.previewUrl = this.documentsService.getPreviewUrl(documentId) this.previewUrl = this.documentsService.getPreviewUrl(documentId)
this.updatePdfSource() this.updatePdfSource()
this.http this.http
@@ -499,10 +539,7 @@ export class DocumentDetailComponent
} else if (openDocument) { } else if (openDocument) {
if (new Date(doc.modified) > new Date(openDocument.modified)) { if (new Date(doc.modified) > new Date(openDocument.modified)) {
if (this.hasLocalEdits(openDocument)) { if (this.hasLocalEdits(openDocument)) {
this.remoteUpdateDetected = true this.showIncomingUpdateModal(doc.modified)
this.remoteUpdateModified = doc.modified
? new Date(doc.modified).toISOString()
: null
} else { } else {
// No local edits to preserve, so keep the tab in sync automatically. // No local edits to preserve, so keep the tab in sync automatically.
Object.assign(openDocument, doc) Object.assign(openDocument, doc)
@@ -549,8 +586,7 @@ export class DocumentDetailComponent
if (!this.document || this.networkActive) return if (!this.document || this.networkActive) return
if (this.openDocumentService.isDirty(this.document)) { if (this.openDocumentService.isDirty(this.document)) {
this.remoteUpdateDetected = true this.showIncomingUpdateModal(data.modified)
this.remoteUpdateModified = data.modified ?? null
} else { } else {
this.docChangeNotifier.next(this.documentId) this.docChangeNotifier.next(this.documentId)
this.loadDocument(this.documentId, true) this.loadDocument(this.documentId, true)
@@ -560,14 +596,10 @@ export class DocumentDetailComponent
} }
} }
dismissRemoteUpdateWarning() { private reloadRemoteVersion() {
this.remoteUpdateDetected = false
this.remoteUpdateModified = null
}
reloadRemoteVersion() {
if (!this.documentId) return if (!this.documentId) return
this.closeIncomingUpdateModal()
this.docChangeNotifier.next(this.documentId) this.docChangeNotifier.next(this.documentId)
this.loadDocument(this.documentId, true) this.loadDocument(this.documentId, true)
this.toastService.showInfo($localize`Document reloaded.`) this.toastService.showInfo($localize`Document reloaded.`)
@@ -970,7 +1002,7 @@ export class DocumentDetailComponent
) )
.subscribe({ .subscribe({
next: (doc) => { next: (doc) => {
this.dismissRemoteUpdateWarning() this.closeIncomingUpdateModal()
Object.assign(this.document, doc) Object.assign(this.document, doc)
doc['permissions_form'] = { doc['permissions_form'] = {
owner: doc.owner, owner: doc.owner,
@@ -1017,7 +1049,7 @@ export class DocumentDetailComponent
.pipe(first()) .pipe(first())
.subscribe({ .subscribe({
next: (docValues) => { next: (docValues) => {
this.dismissRemoteUpdateWarning() this.closeIncomingUpdateModal()
// in case data changed while saving eg removing inbox_tags // in case data changed while saving eg removing inbox_tags
this.documentForm.patchValue(docValues) this.documentForm.patchValue(docValues)
const newValues = Object.assign({}, this.documentForm.value) const newValues = Object.assign({}, this.documentForm.value)
@@ -1097,7 +1129,7 @@ export class DocumentDetailComponent
.pipe(first()) .pipe(first())
.subscribe({ .subscribe({
next: ({ updateResult, nextDocId, closeResult }) => { next: ({ updateResult, nextDocId, closeResult }) => {
this.dismissRemoteUpdateWarning() this.closeIncomingUpdateModal()
this.error = null this.error = null
this.networkActive = false this.networkActive = false
if (closeResult && updateResult && nextDocId) { if (closeResult && updateResult && nextDocId) {