From 0088333360b9dba110242600560b737b1cea2e1b Mon Sep 17 00:00:00 2001 From: shamoon <4887959+shamoon@users.noreply.github.com> Date: Tue, 26 Aug 2025 13:29:38 -0700 Subject: [PATCH] Chore: refactor document details component (#10662) --- .../document-detail.component.ts | 470 +++++++++--------- 1 file changed, 243 insertions(+), 227 deletions(-) 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 302dc9114..29bcf8fa8 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 @@ -21,8 +21,9 @@ import { dirtyCheck, DirtyComponent } from '@ngneat/dirty-check-forms' import { PDFDocumentProxy, PdfViewerModule } from 'ng2-pdf-viewer' import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons' import { DeviceDetectorService } from 'ngx-device-detector' -import { BehaviorSubject, Observable, Subject } from 'rxjs' +import { BehaviorSubject, Observable, of, Subject } from 'rxjs' import { + catchError, debounceTime, distinctUntilChanged, filter, @@ -327,19 +328,163 @@ export class DocumentDetailComponent } } + private mapDocToForm(doc: Document): any { + return { + ...doc, + permissions_form: { owner: doc.owner, set_permissions: doc.permissions }, + } + } + + private mapFormToDoc(value: any): any { + const docValues = { ...value } + docValues['owner'] = value['permissions_form']?.owner + docValues['set_permissions'] = value['permissions_form']?.set_permissions + delete docValues['permissions_form'] + return docValues + } + + private prepareForm(doc: Document): void { + this.documentForm.reset(this.mapDocToForm(doc), { emitEvent: false }) + if (!this.userCanEditDoc(doc)) { + this.documentForm.disable({ emitEvent: false }) + } else { + this.documentForm.enable({ emitEvent: false }) + } + if (doc.__changedFields) { + doc.__changedFields.forEach((field) => { + if (field === 'owner' || field === 'set_permissions') { + this.documentForm.get('permissions_form')?.markAsDirty() + } else { + this.documentForm.get(field)?.markAsDirty() + } + }) + } + } + + private setupDirtyTracking( + currentDocument: Document, + originalDocument: Document + ): void { + this.store = new BehaviorSubject({ + title: originalDocument.title, + content: originalDocument.content, + created: originalDocument.created, + correspondent: originalDocument.correspondent, + document_type: originalDocument.document_type, + storage_path: originalDocument.storage_path, + archive_serial_number: originalDocument.archive_serial_number, + tags: [...originalDocument.tags], + permissions_form: { + owner: originalDocument.owner, + set_permissions: originalDocument.permissions, + }, + custom_fields: [...originalDocument.custom_fields], + }) + this.isDirty$ = dirtyCheck(this.documentForm, this.store.asObservable()) + this.isDirty$ + .pipe( + takeUntil(this.unsubscribeNotifier), + takeUntil(this.docChangeNotifier) + ) + .subscribe((dirty) => + this.openDocumentService.setDirty( + currentDocument, + dirty, + this.getChangedFields() + ) + ) + } + + private loadDocument(documentId: number): void { + this.previewUrl = this.documentsService.getPreviewUrl(documentId) + this.http + .get(this.previewUrl, { responseType: 'text' }) + .pipe( + first(), + takeUntil(this.unsubscribeNotifier), + takeUntil(this.docChangeNotifier) + ) + .subscribe({ + next: (res) => (this.previewText = res.toString()), + error: (err) => + (this.previewText = $localize`An error occurred loading content: ${ + err.message ?? err.toString() + }`), + }) + this.thumbUrl = this.documentsService.getThumbUrl(documentId) + this.documentsService + .get(documentId) + .pipe( + catchError(() => { + // 404 is handled in the subscribe below + return of(null) + }), + first(), + takeUntil(this.unsubscribeNotifier), + takeUntil(this.docChangeNotifier) + ) + .subscribe({ + next: (doc) => { + if (!doc) { + this.router.navigate(['404'], { replaceUrl: true }) + return + } + this.documentId = doc.id + this.suggestions = null + const openDocument = this.openDocumentService.getOpenDocument( + this.documentId + ) + 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() + ) + } + } else { + this.openDocumentService + .openDocument(doc) + .pipe( + first(), + takeUntil(this.unsubscribeNotifier), + takeUntil(this.docChangeNotifier) + ) + .subscribe() + } + this.updateComponent(useDoc) + this.titleSubject + .pipe( + debounceTime(1000), + distinctUntilChanged(), + takeUntil(this.docChangeNotifier), + takeUntil(this.unsubscribeNotifier) + ) + .subscribe((titleValue) => { + if (titleValue !== this.titleInput.value) return + this.title = titleValue + this.documentForm.patchValue({ title: titleValue }) + }) + this.setupDirtyTracking(useDoc, doc) + }, + }) + } + ngOnInit(): void { this.setZoom(this.settings.get(SETTINGS_KEYS.PDF_VIEWER_ZOOM_SETTING)) this.documentForm.valueChanges .pipe(takeUntil(this.unsubscribeNotifier)) - .subscribe(() => { + .subscribe((values) => { this.error = null - const docValues = Object.assign({}, this.documentForm.value) - docValues['owner'] = - this.documentForm.get('permissions_form').value['owner'] - docValues['set_permissions'] = - this.documentForm.get('permissions_form').value['set_permissions'] - delete docValues['permissions_form'] - Object.assign(this.document, docValues) + Object.assign(this.document, this.mapFormToDoc(values)) }) if ( @@ -391,171 +536,36 @@ export class DocumentDetailComponent this.route.paramMap .pipe( - filter((paramMap) => { - // only init when changing docs & section is set - return ( + filter( + (paramMap) => +paramMap.get('id') !== this.documentId && paramMap.get('section')?.length > 0 - ) - }), - takeUntil(this.unsubscribeNotifier), - switchMap((paramMap) => { - const documentId = +paramMap.get('id') - this.docChangeNotifier.next(documentId) - // Dont wait to get the preview - this.previewUrl = this.documentsService.getPreviewUrl(documentId) - this.http.get(this.previewUrl, { responseType: 'text' }).subscribe({ - next: (res) => { - this.previewText = res.toString() - }, - error: (err) => { - this.previewText = $localize`An error occurred loading content: ${ - err.message ?? err.toString() - }` - }, - }) - this.thumbUrl = this.documentsService.getThumbUrl(documentId) - return this.documentsService.get(documentId) - }) + ), + takeUntil(this.unsubscribeNotifier) ) - .pipe( - switchMap((doc) => { - this.documentId = doc.id - this.suggestions = null - const openDocument = this.openDocumentService.getOpenDocument( - this.documentId - ) - - if (openDocument) { - if ( - new Date(doc.modified) > new Date(openDocument.modified) && - !this.modalService.hasOpenModals() - ) { - let 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() - ) - } - - // Prevent mutating stale form values into the next document: only sync if it still matches the active document. - if ( - this.documentForm.dirty && - (this.document?.id === openDocument.id || !this.document) - ) { - Object.assign(openDocument, this.documentForm.value) - openDocument['owner'] = - this.documentForm.get('permissions_form').value['owner'] - openDocument['permissions'] = - this.documentForm.get('permissions_form').value[ - 'set_permissions' - ] - delete openDocument['permissions_form'] - } - if (openDocument.__changedFields) { - openDocument.__changedFields.forEach((field) => { - if (field === 'owner' || field === 'set_permissions') { - this.documentForm.get('permissions_form').markAsDirty() - } else { - this.documentForm.get(field)?.markAsDirty() - } - }) - } - this.updateComponent(openDocument) - } else { - this.openDocumentService.openDocument(doc) - this.updateComponent(doc) - } - - this.titleSubject - .pipe( - debounceTime(1000), - distinctUntilChanged(), - takeUntil(this.docChangeNotifier), - takeUntil(this.unsubscribeNotifier) - ) - .subscribe({ - next: (titleValue) => { - // In the rare case when the field changed just after debounced event was fired. - // We dont want to overwrite what's actually in the text field, so just return - if (titleValue !== this.titleInput.value) return - - this.title = titleValue - this.documentForm.patchValue({ title: titleValue }) - }, - complete: () => { - // doc changed so we manually check dirty in case title was changed - if ( - this.store.getValue().title !== - this.documentForm.get('title').value - ) { - this.openDocumentService.setDirty( - doc, - true, - this.getChangedFields() - ) - } - }, - }) - - // Initialize dirtyCheck - this.store = new BehaviorSubject({ - title: doc.title, - content: doc.content, - created: doc.created, - correspondent: doc.correspondent, - document_type: doc.document_type, - storage_path: doc.storage_path, - archive_serial_number: doc.archive_serial_number, - tags: [...doc.tags], - permissions_form: { - owner: doc.owner, - set_permissions: doc.permissions, - }, - custom_fields: [...doc.custom_fields], - }) - - this.isDirty$ = dirtyCheck( - this.documentForm, - this.store.asObservable() - ) - - return this.isDirty$.pipe( - takeUntil(this.unsubscribeNotifier), - map((dirty) => ({ doc, dirty })) - ) - }) - ) - .subscribe({ - next: ({ doc, dirty }) => { - this.openDocumentService.setDirty(doc, dirty, this.getChangedFields()) - }, - error: (error) => { - this.router.navigate(['404'], { - replaceUrl: true, - }) - }, + .subscribe((paramMap) => { + const documentId = +paramMap.get('id') + this.docChangeNotifier.next(documentId) + this.loadDocument(documentId) }) - this.route.paramMap.subscribe((paramMap) => { - const section = paramMap.get('section') - if (section) { - const navIDKey: string = Object.keys(DocumentDetailNavIDs).find( - (navID) => navID.toLowerCase() == section - ) - if (navIDKey) { - this.activeNavID = DocumentDetailNavIDs[navIDKey] + this.route.paramMap + .pipe(takeUntil(this.unsubscribeNotifier)) + .subscribe((paramMap) => { + const section = paramMap.get('section') + if (section) { + const navIDKey: string = Object.keys(DocumentDetailNavIDs).find( + (navID) => navID.toLowerCase() == section + ) + if (navIDKey) { + this.activeNavID = DocumentDetailNavIDs[navIDKey] + } + } else if (paramMap.get('id')) { + this.router.navigate(['documents', +paramMap.get('id'), 'details'], { + replaceUrl: true, + }) } - } else if (paramMap.get('id')) { - this.router.navigate(['documents', +paramMap.get('id'), 'details'], { - replaceUrl: true, - }) - } - }) + }) this.hotKeyService .addShortcut({ @@ -682,19 +692,7 @@ export class DocumentDetailComponent }) } this.title = this.documentTitlePipe.transform(doc.title) - const docFormValues = Object.assign({}, doc) - docFormValues['permissions_form'] = { - owner: doc.owner, - set_permissions: doc.permissions, - } - - this.documentForm.patchValue(docFormValues, { emitEvent: false }) - if (!this.userCanEdit) this.documentForm.disable() - setTimeout(() => { - // check again after a tick in case form was dirty - if (!this.userCanEdit) this.documentForm.disable() - else this.documentForm.enable() - }, 10) + this.prepareForm(doc) } get customFieldFormFields(): FormArray { @@ -797,7 +795,11 @@ export class DocumentDetailComponent discard() { this.documentsService .get(this.documentId) - .pipe(first()) + .pipe( + first(), + takeUntil(this.unsubscribeNotifier), + takeUntil(this.docChangeNotifier) + ) .subscribe({ next: (doc) => { Object.assign(this.document, doc) @@ -900,9 +902,10 @@ export class DocumentDetailComponent .patch(this.getChangedFields()) .pipe( switchMap((updateResult) => { - return this.documentListViewService - .getNext(this.documentId) - .pipe(map((nextDocId) => ({ nextDocId, updateResult }))) + return this.documentListViewService.getNext(this.documentId).pipe( + map((nextDocId) => ({ nextDocId, updateResult })), + takeUntil(this.unsubscribeNotifier) + ) }) ) .pipe( @@ -912,7 +915,10 @@ export class DocumentDetailComponent return this.openDocumentService .closeDocument(this.document) .pipe( - map((closeResult) => ({ updateResult, nextDocId, closeResult })) + map( + (closeResult) => ({ updateResult, nextDocId, closeResult }), + takeUntil(this.unsubscribeNotifier) + ) ) } }) @@ -1238,16 +1244,19 @@ export class DocumentDetailComponent ) { doc.owner = this.store.value.permissions_form.owner } + return !this.document || this.userCanEditDoc(doc) + } + + private userCanEditDoc(doc: Document): boolean { return ( - !this.document || - (this.permissionsService.currentUserCan( + this.permissionsService.currentUserCan( PermissionAction.Change, PermissionType.Document ) && - this.permissionsService.currentUserHasObjectPermissions( - PermissionAction.Change, - doc - )) + this.permissionsService.currentUserHasObjectPermissions( + PermissionAction.Change, + doc + ) ) } @@ -1429,43 +1438,50 @@ export class DocumentDetailComponent } private tryRenderTiff() { - this.http.get(this.previewUrl, { responseType: 'arraybuffer' }).subscribe({ - next: (res) => { - /* istanbul ignore next */ - try { - // See UTIF.js > _imgLoaded - const tiffIfds: any[] = UTIF.decode(res) - var vsns = tiffIfds, - ma = 0, - page = vsns[0] - if (tiffIfds[0].subIFD) vsns = vsns.concat(tiffIfds[0].subIFD) - for (var i = 0; i < vsns.length; i++) { - var img = vsns[i] - if (img['t258'] == null || img['t258'].length < 3) continue - var ar = img['t256'] * img['t257'] - if (ar > ma) { - ma = ar - page = img + this.http + .get(this.previewUrl, { responseType: 'arraybuffer' }) + .pipe( + first(), + takeUntil(this.unsubscribeNotifier), + takeUntil(this.docChangeNotifier) + ) + .subscribe({ + next: (res) => { + /* istanbul ignore next */ + try { + // See UTIF.js > _imgLoaded + const tiffIfds: any[] = UTIF.decode(res) + var vsns = tiffIfds, + ma = 0, + page = vsns[0] + if (tiffIfds[0].subIFD) vsns = vsns.concat(tiffIfds[0].subIFD) + for (var i = 0; i < vsns.length; i++) { + var img = vsns[i] + if (img['t258'] == null || img['t258'].length < 3) continue + var ar = img['t256'] * img['t257'] + if (ar > ma) { + ma = ar + page = img + } } + UTIF.decodeImage(res, page, tiffIfds) + const rgba = UTIF.toRGBA8(page) + const { width: w, height: h } = page + var cnv = document.createElement('canvas') + cnv.width = w + cnv.height = h + var ctx = cnv.getContext('2d'), + imgd = ctx.createImageData(w, h) + for (var i = 0; i < rgba.length; i++) imgd.data[i] = rgba[i] + ctx.putImageData(imgd, 0, 0) + this.tiffURL = cnv.toDataURL() + } catch (err) { + this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}` } - UTIF.decodeImage(res, page, tiffIfds) - const rgba = UTIF.toRGBA8(page) - const { width: w, height: h } = page - var cnv = document.createElement('canvas') - cnv.width = w - cnv.height = h - var ctx = cnv.getContext('2d'), - imgd = ctx.createImageData(w, h) - for (var i = 0; i < rgba.length; i++) imgd.data[i] = rgba[i] - ctx.putImageData(imgd, 0, 0) - this.tiffURL = cnv.toDataURL() - } catch (err) { + }, + error: (err) => { this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}` - } - }, - error: (err) => { - this.tiffError = $localize`An error occurred loading tiff: ${err.toString()}` - }, - }) + }, + }) } }