Ok finally doc detail

This commit is contained in:
shamoon
2026-07-09 17:44:02 -07:00
parent b769cd09ff
commit 255e0b0147
18 changed files with 496 additions and 678 deletions
@@ -99,6 +99,13 @@ const status: SystemStatus = {
llmindex_status: SystemStatusItemStatus.DISABLED,
llmindex_last_modified: new Date().toISOString(),
llmindex_error: null,
summary: {
days: 30,
total_count: 12,
pending_count: 1,
success_count: 10,
failure_count: 1,
},
},
}
@@ -664,6 +664,6 @@ export class SettingsComponent
size: 'xl',
}
)
modal.componentInstance.status = this.systemStatus
modal.componentInstance.status.set(this.systemStatus)
}
}
@@ -101,9 +101,9 @@ export class UsersAndGroupsComponent
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.dialogMode = user
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(
user ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
modal.componentInstance.object = user
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
@@ -170,9 +170,9 @@ export class UsersAndGroupsComponent
backdrop: 'static',
size: 'lg',
})
modal.componentInstance.dialogMode = group
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(
group ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
modal.componentInstance.object = group
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
@@ -207,7 +207,7 @@ export class GlobalSearchComponent implements OnInit {
editDialogComponent,
{ size }
)
modalRef.componentInstance.dialogMode = EditDialogMode.EDIT
modalRef.componentInstance.dialogMode.set(EditDialogMode.EDIT)
modalRef.componentInstance.object = object
modalRef.componentInstance.succeeded.subscribe(() => {
this.toastService.showInfo($localize`Successfully updated object.`)
@@ -245,7 +245,7 @@ export class GlobalSearchComponent implements OnInit {
editDialogComponent,
{ size }
)
modalRef.componentInstance.dialogMode = EditDialogMode.EDIT
modalRef.componentInstance.dialogMode.set(EditDialogMode.EDIT)
modalRef.componentInstance.object = object
modalRef.componentInstance.succeeded.subscribe(() => {
this.toastService.showInfo($localize`Successfully updated object.`)
@@ -46,9 +46,7 @@ export abstract class EditDialogComponent<
protected settingsService = inject(SettingsService)
protected permissionsService = inject(PermissionsService)
dialogMode = model(EditDialogMode.CREATE, {
equal: () => false,
})
dialogMode = model(EditDialogMode.CREATE)
@Input()
object: T
@@ -176,7 +176,7 @@ export class TagsComponent implements OnInit, ControlValueAccessor {
var modal = this.modalService.open(TagEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
if (name) modal.componentInstance.object = { name: name }
else if (this.select.searchTerm)
modal.componentInstance.object = { name: this.select.searchTerm }
@@ -16,7 +16,6 @@ jest.mock('src/environments/environment', () => ({
import { Clipboard } from '@angular/cdk/clipboard'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { signal } from '@angular/core'
import {
ComponentFixture,
TestBed,
@@ -107,7 +106,7 @@ describe('SystemStatusDialogComponent', () => {
fixture = TestBed.createComponent(SystemStatusDialogComponent)
component = fixture.componentInstance
component.status = signal({ ...status })
component.status.set({ ...status })
clipboard = TestBed.inject(Clipboard)
tasksService = TestBed.inject(TasksService)
systemStatusService = TestBed.inject(SystemStatusService)
@@ -1,10 +1,10 @@
<pngx-page-header [(title)]="title" [id]="documentId">
<pngx-page-header [title]="title()" [id]="documentId()">
@if (archiveContentRenderType === ContentRenderType.PDF && !useNativePdfViewer) {
@if (previewNumPages) {
@if (previewNumPages()) {
<div class="input-group input-group-sm ms-2 d-none d-md-flex">
<div class="input-group-text" i18n>Page</div>
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages" [(ngModel)]="previewCurrentPage" />
<div class="input-group-text" i18n>of {{previewNumPages}}</div>
<input class="form-control flex-grow-0 w-auto" type="number" min="1" [max]="previewNumPages()" [ngModel]="previewCurrentPage()" (ngModelChange)="previewCurrentPage.set($event)" />
<div class="input-group-text" i18n>of {{previewNumPages()}}</div>
</div>
}
<div class="input-group input-group-sm me-md-5 d-none d-md-flex">
@@ -25,9 +25,9 @@
</button>
<pngx-document-version-dropdown
[documentId]="documentId"
[versions]="document?.versions ?? []"
[selectedVersionId]="selectedVersionId"
[documentId]="documentId()"
[versions]="document()?.versions ?? []"
[selectedVersionId]="selectedVersionId()"
[userIsOwner]="userIsOwner"
[userCanEdit]="userCanEdit"
(versionSelected)="onVersionSelected($event)"
@@ -35,8 +35,8 @@
/>
<div class="btn-group">
<button (click)="download()" class="btn btn-sm btn-outline-primary" [disabled]="downloading">
@if (downloading) {
<button (click)="download()" class="btn btn-sm btn-outline-primary" [disabled]="downloading()">
@if (downloading()) {
<div class="spinner-border spinner-border-sm" role="status"></div>
} @else {
<i-bs width="1.2em" height="1.2em" name="download"></i-bs>
@@ -45,10 +45,10 @@
</button>
<div class="btn-group" ngbDropdown role="group">
<button class="btn btn-sm btn-outline-primary dropdown-toggle" [disabled]="downloading" ngbDropdownToggle></button>
<button class="btn btn-sm btn-outline-primary dropdown-toggle" [disabled]="downloading()" ngbDropdownToggle></button>
<div class="dropdown-menu shadow" ngbDropdownMenu>
@if (metadata?.has_archive_version) {
<button ngbDropdownItem (click)="download(true)" [disabled]="downloading" i18n>Download original</button>
@if (metadata()?.has_archive_version) {
<button ngbDropdownItem (click)="download(true)" [disabled]="downloading()" i18n>Download original</button>
<div class="dropdown-divider"></div>
}
<form class="px-3 py-1">
@@ -129,9 +129,9 @@
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<div class="btn-group pb-3 ms-auto">
<pngx-suggestions-dropdown *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }"
[disabled]="!userCanEdit || suggestionsLoading"
[loading]="suggestionsLoading"
[suggestions]="suggestions"
[disabled]="!userCanEdit || suggestionsLoading()"
[loading]="suggestionsLoading()"
[suggestions]="suggestions()"
[aiEnabled]="aiEnabled"
(getSuggestions)="getSuggestions()"
(addTag)="createTag($event)"
@@ -143,9 +143,9 @@
<div class="btn-group pb-3 ms-2">
<pngx-custom-fields-dropdown
*pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.CustomField }"
[documentId]="documentId"
[documentId]="documentId()"
[disabled]="!userCanEdit"
[existingFields]="document?.custom_fields"
[existingFields]="document()?.custom_fields"
(created)="refreshCustomFields()"
(added)="addField($event)">
</pngx-custom-fields-dropdown>
@@ -155,33 +155,33 @@
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
</div>
<ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [(activeId)]="activeNavID">
<ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [activeId]="activeNavID()" (activeIdChange)="activeNavID.set($event)">
<li [ngbNavItem]="DocumentDetailNavIDs.Details">
<a ngbNavLink i18n>Details</a>
<ng-template ngbNavContent>
<div>
<pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" [suggestion]="suggestions?.title" (keyup)="titleKeyUp($event)" [error]="error?.title"></pngx-input-text>
<pngx-input-text #inputTitle i18n-title title="Title" formControlName="title" [horizontal]="true" [suggestion]="suggestions()?.title" (keyup)="titleKeyUp($event)" [error]="error()?.title"></pngx-input-text>
@if (!isFieldHidden(DocumentDetailFieldID.ArchiveSerialNumber)) {
<pngx-input-number i18n-title title="Archive serial number" [error]="error?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
<pngx-input-number i18n-title title="Archive serial number" [error]="error()?.archive_serial_number" [horizontal]="true" formControlName='archive_serial_number'></pngx-input-number>
}
<pngx-input-date i18n-title title="Date created" formControlName="created" [suggestions]="suggestions?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
[error]="error?.created"></pngx-input-date>
<pngx-input-date i18n-title title="Date created" formControlName="created" [suggestions]="suggestions()?.dates" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event)"
[error]="error()?.created"></pngx-input-date>
@if (!isFieldHidden(DocumentDetailFieldID.Correspondent)) {
<pngx-input-select [items]="correspondents" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Correspondent)"
(createNew)="createCorrespondent($event)" [hideAddButton]="createDisabled(DataType.Correspondent)" [suggestions]="suggestions?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select>
<pngx-input-select [items]="correspondents()" i18n-title title="Correspondent" formControlName="correspondent" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Correspondent)"
(createNew)="createCorrespondent($event)" [hideAddButton]="createDisabled(DataType.Correspondent)" [suggestions]="suggestions()?.correspondents" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Correspondent }"></pngx-input-select>
}
@if (!isFieldHidden(DocumentDetailFieldID.DocumentType)) {
<pngx-input-select [items]="documentTypes" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.DocumentType)"
(createNew)="createDocumentType($event)" [hideAddButton]="createDisabled(DataType.DocumentType)" [suggestions]="suggestions?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"></pngx-input-select>
<pngx-input-select [items]="documentTypes()" i18n-title title="Document type" formControlName="document_type" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.DocumentType)"
(createNew)="createDocumentType($event)" [hideAddButton]="createDisabled(DataType.DocumentType)" [suggestions]="suggestions()?.document_types" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.DocumentType }"></pngx-input-select>
}
@if (!isFieldHidden(DocumentDetailFieldID.StoragePath)) {
<pngx-input-select [items]="storagePaths" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.StoragePath)"
(createNew)="createStoragePath($event)" [hideAddButton]="createDisabled(DataType.StoragePath)" [suggestions]="suggestions?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
<pngx-input-select [items]="storagePaths()" i18n-title title="Storage path" formControlName="storage_path" [allowNull]="true" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.StoragePath)"
(createNew)="createStoragePath($event)" [hideAddButton]="createDisabled(DataType.StoragePath)" [suggestions]="suggestions()?.storage_paths" i18n-placeholder placeholder="Default" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.StoragePath }"></pngx-input-select>
}
@if (!isFieldHidden(DocumentDetailFieldID.Tags)) {
<pngx-input-tags #tagsInput formControlName="tags" [suggestions]="suggestions?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Tag)" [hideAddButton]="createDisabled(DataType.Tag)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
<pngx-input-tags #tagsInput formControlName="tags" [suggestions]="suggestions()?.tags" [showFilter]="true" [horizontal]="true" (filterDocuments)="filterDocuments($event, DataType.Tag)" [hideAddButton]="createDisabled(DataType.Tag)" *pngxIfPermissions="{ action: PermissionAction.View, type: PermissionType.Tag }"></pngx-input-tags>
}
@for (fieldInstance of document?.custom_fields; track fieldInstance.field; let i = $index) {
@for (fieldInstance of document()?.custom_fields; track fieldInstance.field; let i = $index) {
<div [formGroup]="customFieldFormFields.controls[i]">
@switch (getCustomFieldFromInstance(fieldInstance)?.data_type) {
@case (CustomFieldDataType.String) {
@@ -246,7 +246,7 @@
@case (CustomFieldDataType.DocumentLink) {
<pngx-input-document-link formControlName="value"
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
[parentDocumentID]="documentId"
[parentDocumentID]="documentId()"
[removable]="userCanEdit"
(removed)="removeField(fieldInstance)"
[horizontal]="true"
@@ -295,58 +295,58 @@
<a ngbNavLink i18n>Metadata</a>
<ng-template ngbNavContent>
@if (document) {
@if (document()) {
<table class="table table-borderless">
<tbody>
<tr>
<td i18n>Date modified</td>
<td>{{document.modified | customDate}}</td>
<td>{{document().modified | customDate}}</td>
</tr>
<tr>
<td i18n>Date added</td>
<td>{{document.added | customDate}}</td>
<td>{{document().added | customDate}}</td>
</tr>
<tr>
<td i18n>Media filename</td>
<td>{{metadata?.media_filename}}</td>
<td>{{metadata()?.media_filename}}</td>
</tr>
<tr>
<td i18n>Original filename</td>
<td>{{metadata?.original_filename}}</td>
<td>{{metadata()?.original_filename}}</td>
</tr>
<tr>
<td i18n>Original MD5 checksum</td>
<td>{{metadata?.original_checksum}}</td>
<td>{{metadata()?.original_checksum}}</td>
</tr>
<tr>
<td i18n>Original file size</td>
<td>{{metadata?.original_size | fileSize}}</td>
<td>{{metadata()?.original_size | fileSize}}</td>
</tr>
<tr>
<td i18n>Original mime type</td>
<td>{{metadata?.original_mime_type}}</td>
<td>{{metadata()?.original_mime_type}}</td>
</tr>
@if (metadata?.has_archive_version) {
@if (metadata()?.has_archive_version) {
<tr>
<td i18n>Archive MD5 checksum</td>
<td>{{metadata?.archive_checksum}}</td>
<td>{{metadata()?.archive_checksum}}</td>
</tr>
}
@if (metadata?.has_archive_version) {
@if (metadata()?.has_archive_version) {
<tr>
<td i18n>Archive file size</td>
<td>{{metadata?.archive_size | fileSize}}</td>
<td>{{metadata()?.archive_size | fileSize}}</td>
</tr>
}
</tbody>
</table>
}
@if (metadata?.original_metadata?.length > 0) {
<pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata.original_metadata"></pngx-metadata-collapse>
@if (metadata()?.original_metadata?.length > 0) {
<pngx-metadata-collapse i18n-title title="Original document metadata" [metadata]="metadata()?.original_metadata"></pngx-metadata-collapse>
}
@if (metadata?.archive_metadata?.length > 0) {
<pngx-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata.archive_metadata"></pngx-metadata-collapse>
@if (metadata()?.archive_metadata?.length > 0) {
<pngx-metadata-collapse i18n-title title="Archived document metadata" [metadata]="metadata()?.archive_metadata"></pngx-metadata-collapse>
}
</ng-template>
@@ -365,11 +365,11 @@
@if (notesEnabled) {
<li [ngbNavItem]="DocumentDetailNavIDs.Notes">
<a class="text-nowrap" ngbNavLink i18n>Notes @if (document?.notes.length) {
<span class="badge text-bg-secondary ms-1">{{document.notes.length}}</span>
<a class="text-nowrap" ngbNavLink i18n>Notes @if (document()?.notes.length) {
<span class="badge text-bg-secondary ms-1">{{document().notes.length}}</span>
}</a>
<ng-template ngbNavContent>
<pngx-document-notes [documentId]="documentId" [notes]="document?.notes" [addDisabled]="!userCanEdit" (updated)="notesUpdated($event)"></pngx-document-notes>
<pngx-document-notes [documentId]="documentId()" [notes]="document()?.notes" [addDisabled]="!userCanEdit" (updated)="notesUpdated($event)"></pngx-document-notes>
</ng-template>
</li>
}
@@ -379,7 +379,7 @@
<a ngbNavLink i18n>History</a>
<ng-template ngbNavContent>
<div class="mb-3">
<pngx-document-history [documentId]="documentId"></pngx-document-history>
<pngx-document-history [documentId]="documentId()"></pngx-document-history>
</div>
</ng-template>
</li>
@@ -390,23 +390,23 @@
<a ngbNavLink i18n>Permissions</a>
<ng-template ngbNavContent>
<div class="mb-3">
<pngx-permissions-form [users]="users" formControlName="permissions_form"></pngx-permissions-form>
<pngx-permissions-form [users]="users()" formControlName="permissions_form"></pngx-permissions-form>
</div>
</ng-template>
</li>
}
@if (document?.duplicate_documents?.length) {
@if (document()?.duplicate_documents?.length) {
<li [ngbNavItem]="DocumentDetailNavIDs.Duplicates">
<a class="text-nowrap" ngbNavLink i18n>
Duplicates
<span class="badge text-bg-secondary ms-1">{{ document.duplicate_documents.length }}</span>
<span class="badge text-bg-secondary ms-1">{{ document().duplicate_documents.length }}</span>
</a>
<ng-template ngbNavContent>
<div class="d-flex flex-column gap-2">
<div class="fst-italic" i18n>Duplicate documents detected:</div>
<div class="list-group">
@for (duplicate of document.duplicate_documents; track duplicate.id) {
@for (duplicate of document()?.duplicate_documents; track duplicate.id) {
<a
class="list-group-item list-group-item-action d-flex justify-content-between align-items-center"
[routerLink]="['/documents', duplicate.id, 'details']"
@@ -442,21 +442,21 @@
<ng-template #saveButtons>
<div class="btn-group pb-3 ms-4">
<ng-container *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save</button>
<button type="submit" class="order-3 btn btn-sm btn-primary" i18n [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">Save</button>
@if (hasNext()) {
<button type="button" class="order-1 btn btn-sm btn-outline-primary" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; next</button>
<button type="button" class="order-1 btn btn-sm btn-outline-primary" (click)="saveEditNext()" i18n [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">Save &amp; next</button>
} @else {
<button type="button" class="order-2 btn btn-sm btn-outline-primary" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Save &amp; close</button>
<button type="button" class="order-2 btn btn-sm btn-outline-primary" (click)="save(true)" i18n [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">Save &amp; close</button>
}
<button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive || (isDirty$ | async) !== true">Discard</button>
<button type="button" class="order-0 btn btn-sm btn-outline-secondary" (click)="discard()" i18n [disabled]="!userCanEdit || networkActive() || (isDirty$ | async) !== true">Discard</button>
</ng-container>
</div>
</ng-template>
<ng-template #previewContent>
<div class="thumb-preview position-absolute pe-none text-center" [class.fade]="previewLoaded">
<div class="thumb-preview position-absolute pe-none text-center" [class.fade]="previewLoaded()">
@if (showThumbnailOverlay) {
<img [src]="thumbUrl" class="mx-auto" [attr.width]="previewZoomScale === 'page-fit' ? 'auto' : '100%'" [attr.height]="previewZoomScale === 'page-fit' ? '100%' : 'auto'" alt="Document loading..." i18n-alt />
<img [src]="thumbUrl()" class="mx-auto" [attr.width]="previewZoomScale() === 'page-fit' ? 'auto' : '100%'" [attr.height]="previewZoomScale() === 'page-fit' ? '100%' : 'auto'" alt="Document loading..." i18n-alt />
}
<div class="position-absolute top-0 start-0 m-2 p-2 d-flex align-items-center justify-content-center">
<div>
@@ -465,45 +465,45 @@
</div>
</div>
</div>
@if (document) {
@if (document()) {
@switch (archiveContentRenderType) {
@case (ContentRenderType.PDF) {
@if (!useNativePdfViewer) {
<div class="preview-sticky pdf-viewer-container">
<pngx-pdf-viewer
[src]="pdfSource"
[password]="pdfPassword"
[src]="pdfSource()"
[password]="pdfPassword()"
[renderMode]="PdfRenderMode.All"
[(page)]="previewCurrentPage"
[zoomScale]="previewZoomScale"
[zoom]="previewZoomSetting"
[page]="previewCurrentPage()" (pageChange)="previewCurrentPage.set($event)"
[zoomScale]="previewZoomScale()"
[zoom]="previewZoomSetting()"
(loadError)="onError($event)"
(afterLoadComplete)="pdfPreviewLoaded($event)">
</pngx-pdf-viewer>
</div>
} @else {
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%"></object>
}
}
@case (ContentRenderType.Text) {
<div class="preview-sticky bg-light p-3 overflow-auto whitespace-preserve" width="100%">{{previewText}}</div>
<div class="preview-sticky bg-light p-3 overflow-auto whitespace-preserve" width="100%">{{previewText()}}</div>
}
@case (ContentRenderType.Image) {
<div class="preview-sticky">
<img [src]="previewUrl" width="100%" height="100%" alt="{{title}}" />
<img [src]="previewUrl()" width="100%" height="100%" alt="{{title()}}" />
</div>
}
@case (ContentRenderType.TIFF) {
@if (!tiffError) {
@if (!tiffError()) {
<div class="preview-sticky">
<img [src]="tiffURL" width="100%" height="100%" alt="{{title}}" />
<img [src]="tiffURL()" width="100%" height="100%" alt="{{title()}}" />
</div>
} @else {
<div class="preview-sticky bg-light p-3 overflow-auto whitespace-preserve" width="100%">{{tiffError}}</div>
<div class="preview-sticky bg-light p-3 overflow-auto whitespace-preserve" width="100%">{{tiffError()}}</div>
}
}
@case (ContentRenderType.Other) {
<object [data]="previewUrl | safeUrl" class="preview-sticky" width="100%"></object>
<object [data]="previewUrl() | safeUrl" class="preview-sticky" width="100%"></object>
}
}
@if (requiresPassword) {
@@ -325,7 +325,9 @@ describe('DocumentDetailComponent', () => {
.spyOn(openDocumentsService, 'openDocument')
.mockReturnValueOnce(of(true))
fixture.detectChanges()
expect(component.activeNavID).toEqual(component.DocumentDetailNavIDs.Notes)
expect(component.activeNavID()).toEqual(
component.DocumentDetailNavIDs.Notes
)
})
it('should switch from preview to details when pdf preview enters the DOM', fakeAsync(() => {
@@ -396,7 +398,7 @@ describe('DocumentDetailComponent', () => {
it('should load non-open document via param', () => {
initNormally()
expect(component.document).toEqual(doc)
expect(component.document()).toEqual(doc)
})
it('should redirect to root when opening a version document id', () => {
@@ -423,7 +425,7 @@ describe('DocumentDetailComponent', () => {
)
fixture.detectChanges()
httpTestingController.expectOne(component.previewUrl).flush('preview')
httpTestingController.expectOne(component.previewUrl()).flush('preview')
expect(getRootSpy).toHaveBeenCalledWith(10)
expect(navigateSpy).toHaveBeenCalledWith(['documents', 3, 'details'], {
@@ -455,7 +457,7 @@ describe('DocumentDetailComponent', () => {
)
fixture.detectChanges()
httpTestingController.expectOne(component.previewUrl).flush('preview')
httpTestingController.expectOne(component.previewUrl()).flush('preview')
expect(navigateSpy).toHaveBeenCalledWith(['404'], { replaceUrl: true })
})
@@ -501,7 +503,7 @@ describe('DocumentDetailComponent', () => {
)
fixture.detectChanges()
httpTestingController.expectOne(component.previewUrl).flush('preview')
httpTestingController.expectOne(component.previewUrl()).flush('preview')
fixture.detectChanges()
const deleteButtons = fixture.debugElement.queryAll(
@@ -512,12 +514,12 @@ describe('DocumentDetailComponent', () => {
it('should fall back to details tab when duplicates tab is active but no duplicates', () => {
initNormally()
component.activeNavID = component.DocumentDetailNavIDs.Duplicates
component.activeNavID.set(component.DocumentDetailNavIDs.Duplicates)
const noDupDoc = { ...doc, duplicate_documents: [] }
component.updateComponent(noDupDoc)
expect(component.activeNavID).toEqual(
expect(component.activeNavID()).toEqual(
component.DocumentDetailNavIDs.Details
)
})
@@ -534,7 +536,7 @@ describe('DocumentDetailComponent', () => {
})
)
fixture.detectChanges() // calls ngOnInit
expect(component.document).toEqual(doc)
expect(component.document()).toEqual(doc)
})
it('should update cached open document duplicates when reloading an open doc', () => {
@@ -578,10 +580,10 @@ describe('DocumentDetailComponent', () => {
it('should not attempt to retrieve objects if user does not have permissions', () => {
currentUserCan = false
initNormally()
expect(component.correspondents).toBeUndefined()
expect(component.documentTypes).toBeUndefined()
expect(component.storagePaths).toBeUndefined()
expect(component.users).toBeUndefined()
expect(component.correspondents()).toBeUndefined()
expect(component.documentTypes()).toBeUndefined()
expect(component.storagePaths()).toBeUndefined()
expect(component.users()).toBeUndefined()
httpTestingController.expectNone(`${environment.apiBaseUrl}documents/tags/`)
httpTestingController.expectNone(
`${environment.apiBaseUrl}documents/correspondents/`
@@ -597,9 +599,9 @@ describe('DocumentDetailComponent', () => {
it('should support creating tag, remove from suggestions', () => {
initNormally()
component.suggestions = {
component.suggestions.set({
suggested_tags: ['Tag1', 'NewTag12'],
}
})
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
const modalSpy = jest.spyOn(modalService, 'open')
@@ -613,14 +615,14 @@ describe('DocumentDetailComponent', () => {
text_color: '#000000',
})
expect(component.tagsInput.value).toContain(12)
expect(component.suggestions.suggested_tags).not.toContain('NewTag12')
expect(component.suggestions().suggested_tags).not.toContain('NewTag12')
})
it('should support creating document type, remove from suggestions', () => {
initNormally()
component.suggestions = {
component.suggestions.set({
suggested_document_types: ['DocumentType1', 'NewDocType2'],
}
})
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
const modalSpy = jest.spyOn(modalService, 'open')
@@ -628,16 +630,16 @@ describe('DocumentDetailComponent', () => {
expect(modalSpy).toHaveBeenCalled()
openModal.componentInstance.succeeded.next({ id: 12, name: 'NewDocType12' })
expect(component.documentForm.get('document_type').value).toEqual(12)
expect(component.suggestions.suggested_document_types).not.toContain(
expect(component.suggestions().suggested_document_types).not.toContain(
'NewDocType2'
)
})
it('should support creating correspondent, remove from suggestions', () => {
initNormally()
component.suggestions = {
component.suggestions.set({
suggested_correspondents: ['Correspondent1', 'NewCorrrespondent12'],
}
})
let openModal: NgbModalRef
modalService.activeInstances.subscribe((modal) => (openModal = modal[0]))
const modalSpy = jest.spyOn(modalService, 'open')
@@ -648,7 +650,7 @@ describe('DocumentDetailComponent', () => {
name: 'NewCorrrespondent12',
})
expect(component.documentForm.get('correspondent').value).toEqual(12)
expect(component.suggestions.suggested_correspondents).not.toContain(
expect(component.suggestions().suggested_correspondents).not.toContain(
'NewCorrrespondent12'
)
})
@@ -669,15 +671,15 @@ describe('DocumentDetailComponent', () => {
it('should allow dischard changes', () => {
initNormally()
component.title = 'Foo Bar'
component.title.set('Foo Bar')
fixture.detectChanges()
jest.spyOn(documentService, 'get').mockReturnValueOnce(of(doc))
component.discard()
fixture.detectChanges()
expect(component.title).toEqual(doc.title)
expect(component.title()).toEqual(doc.title)
expect(openDocumentsService.hasDirty()).toBeFalsy()
// this time with error, mostly for coverage
component.title = 'Foo Bar'
component.title.set('Foo Bar')
fixture.detectChanges()
const navigateSpy = jest.spyOn(router, 'navigate')
jest
@@ -694,10 +696,10 @@ describe('DocumentDetailComponent', () => {
getSpy.mockClear()
getSpy.mockReturnValueOnce(of(doc))
component.selectedVersionId = 10
component.selectedVersionId.set(10)
component.discard()
expect(getSpy).toHaveBeenCalledWith(component.documentId, 10)
expect(getSpy).toHaveBeenCalledWith(component.documentId(), 10)
})
it('should 404 on invalid id', () => {
@@ -724,7 +726,7 @@ describe('DocumentDetailComponent', () => {
it('should support save, close and show success toast', () => {
initNormally()
component.title = 'Foo Bar'
component.title.set('Foo Bar')
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const toastSpy = jest.spyOn(toastService, 'showInfo')
@@ -739,7 +741,7 @@ describe('DocumentDetailComponent', () => {
it('should support save without close and show success toast', () => {
initNormally()
component.title = 'Foo Bar'
component.title.set('Foo Bar')
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const toastSpy = jest.spyOn(toastService, 'showInfo')
@@ -754,7 +756,7 @@ describe('DocumentDetailComponent', () => {
it('save should target currently selected version', () => {
initNormally()
component.selectedVersionId = 10
component.selectedVersionId.set(10)
const patchSpy = jest.spyOn(documentService, 'patch')
patchSpy.mockReturnValue(of(doc))
@@ -767,7 +769,7 @@ describe('DocumentDetailComponent', () => {
it('should show toast error on save if error occurs', () => {
currentUserHasObjectPermissions = true
initNormally()
component.title = 'Foo Bar'
component.title.set('Foo Bar')
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const toastSpy = jest.spyOn(toastService, 'showError')
@@ -785,7 +787,7 @@ describe('DocumentDetailComponent', () => {
it('should show error toast on save but close if user can no longer edit', () => {
currentUserHasObjectPermissions = false
initNormally()
component.title = 'Foo Bar'
component.title.set('Foo Bar')
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const toastSpy = jest.spyOn(toastService, 'showInfo')
@@ -803,7 +805,7 @@ describe('DocumentDetailComponent', () => {
it('should allow save and next', () => {
initNormally()
const nextDocId = 100
component.title = 'Foo Bar'
component.title.set('Foo Bar')
const patchSpy = jest.spyOn(documentService, 'patch')
patchSpy.mockReturnValue(of(doc))
const nextSpy = jest.spyOn(documentListViewService, 'getNext')
@@ -821,7 +823,7 @@ describe('DocumentDetailComponent', () => {
it('should show toast error on save & next if error occurs', () => {
currentUserHasObjectPermissions = true
initNormally()
component.title = 'Foo Bar'
component.title.set('Foo Bar')
const closeSpy = jest.spyOn(component, 'close')
const patchSpy = jest.spyOn(documentService, 'patch')
const toastSpy = jest.spyOn(toastService, 'showError')
@@ -853,7 +855,7 @@ describe('DocumentDetailComponent', () => {
.find((b) => b.nativeElement.textContent === 'Save & next')
).toBeUndefined()
nextSpy.mockReturnValue(true)
component.networkActive = true
component.networkActive.set(true)
fixture.detectChanges()
expect(
fixture.debugElement
@@ -1015,7 +1017,7 @@ describe('DocumentDetailComponent', () => {
it('should support Enter key in password field', () => {
initNormally()
component.metadata = { has_archive_version: true }
component.metadata.set({ has_archive_version: true })
component.onError({ name: 'PasswordException' }) // normally dispatched by pdf viewer
fixture.detectChanges()
expect(component.password).toBeUndefined()
@@ -1030,45 +1032,45 @@ describe('DocumentDetailComponent', () => {
it('should update n pages after pdf loaded', () => {
initNormally()
component.pdfPreviewLoaded({ numPages: 1000 } as any)
expect(component.previewNumPages).toEqual(1000)
expect(component.previewNumPages()).toEqual(1000)
})
it('should include delay of 300ms after previewloaded before showing pdf', fakeAsync(() => {
initNormally()
expect(component.previewLoaded).toBeFalsy()
expect(component.previewLoaded()).toBeFalsy()
component.pdfPreviewLoaded({ numPages: 1000 } as any)
expect(component.previewNumPages).toEqual(1000)
expect(component.previewNumPages()).toEqual(1000)
tick(300)
expect(component.previewLoaded).toBeTruthy()
expect(component.previewLoaded()).toBeTruthy()
}))
it('should support zoom controls', () => {
initNormally()
component.setZoom(PdfZoomLevel.One) // from select
expect(component.previewZoomSetting).toEqual('1')
expect(component.previewZoomSetting()).toEqual('1')
component.increaseZoom()
expect(component.previewZoomSetting).toEqual('1.5')
expect(component.previewZoomSetting()).toEqual('1.5')
component.increaseZoom()
expect(component.previewZoomSetting).toEqual('2')
expect(component.previewZoomSetting()).toEqual('2')
component.decreaseZoom()
expect(component.previewZoomSetting).toEqual('1.5')
expect(component.previewZoomSetting()).toEqual('1.5')
component.setZoom(PdfZoomLevel.One) // from select
component.decreaseZoom()
expect(component.previewZoomSetting).toEqual('.75')
expect(component.previewZoomSetting()).toEqual('.75')
component.setZoom(PdfZoomScale.PageFit) // from select
expect(component.previewZoomScale).toEqual('page-fit')
expect(component.previewZoomSetting).toEqual('1')
expect(component.previewZoomScale()).toEqual('page-fit')
expect(component.previewZoomSetting()).toEqual('1')
component.increaseZoom()
expect(component.previewZoomSetting).toEqual('1.5')
expect(component.previewZoomScale).toEqual('page-width')
expect(component.previewZoomSetting()).toEqual('1.5')
expect(component.previewZoomScale()).toEqual('page-width')
component.setZoom(PdfZoomScale.PageFit) // from select
expect(component.previewZoomScale).toEqual('page-fit')
expect(component.previewZoomSetting).toEqual('1')
expect(component.previewZoomScale()).toEqual('page-fit')
expect(component.previewZoomSetting()).toEqual('1')
component.decreaseZoom()
expect(component.previewZoomSetting).toEqual('.5')
expect(component.previewZoomScale).toEqual('page-width')
expect(component.previewZoomSetting()).toEqual('.5')
expect(component.previewZoomScale()).toEqual('page-width')
})
it('should select correct zoom setting in dropdown', () => {
@@ -1089,7 +1091,7 @@ describe('DocumentDetailComponent', () => {
initNormally()
const refreshSpy = jest.spyOn(openDocumentsService, 'refreshDocument')
component.notesUpdated(notes) // called by notes component
expect(component.document.notes).toEqual(notes)
expect(component.document().notes).toEqual(notes)
expect(refreshSpy).toHaveBeenCalled()
})
@@ -1188,7 +1190,7 @@ describe('DocumentDetailComponent', () => {
it('should detect RTL languages and add css class to content textarea', () => {
initNormally()
component.metadata = { lang: 'he' }
component.metadata.set({ lang: 'he' })
component.nav.select(2) // content
fixture.detectChanges()
expect(component.isRTL).toBeTruthy()
@@ -1197,7 +1199,7 @@ describe('DocumentDetailComponent', () => {
it('should display built-in pdf viewer if not disabled', () => {
initNormally()
component.document.archived_file_name = 'file.pdf'
component.document().archived_file_name = 'file.pdf'
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, false)
expect(component.useNativePdfViewer).toBeFalsy()
fixture.detectChanges()
@@ -1206,7 +1208,7 @@ describe('DocumentDetailComponent', () => {
it('should display native pdf viewer if enabled', () => {
initNormally()
component.document.archived_file_name = 'file.pdf'
component.document().archived_file_name = 'file.pdf'
settingsService.set(SETTINGS_KEYS.USE_NATIVE_PDF_VIEWER, true)
expect(component.useNativePdfViewer).toBeTruthy()
fixture.detectChanges()
@@ -1224,12 +1226,12 @@ describe('DocumentDetailComponent', () => {
const metadataSpy = jest.spyOn(documentService, 'getMetadata')
metadataSpy.mockReturnValue(of({ has_archive_version: true }))
initNormally()
httpTestingController.expectOne(component.previewUrl).flush('preview')
httpTestingController.expectOne(component.previewUrl()).flush('preview')
expect(metadataSpy).toHaveBeenCalledWith(doc.id, null)
metadataSpy.mockClear()
component.document.versions = [
component.document().versions = [
{ id: doc.id, is_root: true },
{ id: 10, is_root: false },
] as any
@@ -1268,7 +1270,7 @@ describe('DocumentDetailComponent', () => {
expect(component.customFieldFormFields).toHaveLength(initialLength)
component.addField(customFields[1])
fixture.detectChanges()
expect(component.document.custom_fields).toHaveLength(initialLength + 1)
expect(component.document().custom_fields).toHaveLength(initialLength + 1)
expect(component.customFieldFormFields).toHaveLength(initialLength + 1)
expect(fixture.debugElement.nativeElement.textContent).toContain(
customFields[1].name
@@ -1288,7 +1290,7 @@ describe('DocumentDetailComponent', () => {
expect(component.customFieldFormFields).toHaveLength(initialLength)
component.removeField(doc.custom_fields[0])
fixture.detectChanges()
expect(component.document.custom_fields).toHaveLength(initialLength - 1)
expect(component.document().custom_fields).toHaveLength(initialLength - 1)
expect(component.customFieldFormFields).toHaveLength(initialLength - 1)
expect(
fixture.debugElement.query(By.css('form ul')).nativeElement.textContent
@@ -1356,7 +1358,7 @@ describe('DocumentDetailComponent', () => {
it('should show custom field errors', () => {
initNormally()
component.error = {
component.error.set({
custom_fields: [
{},
{},
@@ -1364,7 +1366,7 @@ describe('DocumentDetailComponent', () => {
{},
{ non_field_errors: ['Enter a valid URL.'] },
],
}
})
expect(component.getCustomFieldError(2)).toEqual([
'This field may not be null.',
])
@@ -1394,7 +1396,7 @@ describe('DocumentDetailComponent', () => {
initNormally()
expect(suggestionsSpy).toHaveBeenCalled()
expect(aiSuggestionsSpy).not.toHaveBeenCalled()
expect(component.suggestions).toEqual({
expect(component.suggestions()).toEqual({
tags: [42, 43],
suggested_tags: [],
suggested_document_types: [],
@@ -1422,7 +1424,7 @@ describe('DocumentDetailComponent', () => {
initNormally()
expect(suggestionsSpy).not.toHaveBeenCalled()
expect(aiSuggestionsSpy).toHaveBeenCalled()
expect(component.suggestions).toEqual({
expect(component.suggestions()).toEqual({
tags: [42, 43],
suggested_tags: [],
suggested_document_types: [],
@@ -1475,7 +1477,7 @@ describe('DocumentDetailComponent', () => {
it('should react to websocket document updated notifications', () => {
initNormally()
const updateMessage = {
document_id: component.documentId,
document_id: component.documentId(),
modified: '2026-02-17T00:00:00Z',
owner_id: 1,
}
@@ -1494,18 +1496,18 @@ describe('DocumentDetailComponent', () => {
const loadSpy = jest.spyOn(component as any, 'loadDocument')
const toastSpy = jest.spyOn(toastService, 'showInfo')
component.networkActive = true
component.networkActive.set(true)
;(component as any).handleIncomingDocumentUpdated({
document_id: component.documentId,
document_id: component.documentId(),
modified: '2026-02-17T00:00:00Z',
})
expect(loadSpy).not.toHaveBeenCalled()
component.networkActive = false
component.networkActive.set(false)
;(component as any).flushPendingIncomingUpdate()
expect(loadSpy).toHaveBeenCalledWith(component.documentId, true)
expect(loadSpy).toHaveBeenCalledWith(component.documentId(), true)
expect(toastSpy).toHaveBeenCalledWith(
'Document reloaded with latest changes.'
)
@@ -1516,14 +1518,14 @@ describe('DocumentDetailComponent', () => {
const loadSpy = jest.spyOn(component as any, 'loadDocument')
const toastSpy = jest.spyOn(toastService, 'showInfo')
component.networkActive = true
component.networkActive.set(true)
;(component as any).lastLocalSaveModified = '2026-02-17T00:00:00+00:00'
;(component as any).handleIncomingDocumentUpdated({
document_id: component.documentId,
document_id: component.documentId(),
modified: '2026-02-17T00:00:00+00:00',
})
component.networkActive = false
component.networkActive.set(false)
;(component as any).flushPendingIncomingUpdate()
expect(loadSpy).not.toHaveBeenCalled()
@@ -1531,13 +1533,13 @@ describe('DocumentDetailComponent', () => {
})
it('should clear pdf source if preview URL is empty', () => {
component.pdfSource = '/preview'
component.pdfPassword = 'secret'
component.previewUrl = null
component.pdfSource.set('/preview')
component.pdfPassword.set('secret')
component.previewUrl.set(null)
;(component as any).updatePdfSource()
expect(component.pdfSource).toEqual(null)
expect(component.pdfPassword).toBeUndefined()
expect(component.pdfSource()).toEqual(null)
expect(component.pdfPassword()).toBeUndefined()
})
it('should close incoming update modal if one is open', () => {
@@ -1607,7 +1609,7 @@ describe('DocumentDetailComponent', () => {
const loadSpy = jest.spyOn(component as any, 'loadDocument')
;(component as any).handleIncomingDocumentUpdated({
document_id: component.documentId + 1,
document_id: component.documentId() + 1,
modified: '2026-02-17T00:00:00Z',
})
@@ -1622,7 +1624,7 @@ describe('DocumentDetailComponent', () => {
.mockImplementation(() => {})
;(component as any).handleIncomingDocumentUpdated({
document_id: component.documentId,
document_id: component.documentId(),
modified: '2026-02-17T00:00:00Z',
})
@@ -1630,7 +1632,7 @@ describe('DocumentDetailComponent', () => {
})
it('should reload current document and show toast when reloading remote version', () => {
component.documentId = doc.id
component.documentId.set(doc.id)
const closeModalSpy = jest
.spyOn(component as any, 'closeIncomingUpdateModal')
.mockImplementation(() => {})
@@ -1650,7 +1652,7 @@ describe('DocumentDetailComponent', () => {
it('should change preview element by render type', () => {
initNormally()
component.document.archived_file_name = 'file.pdf'
component.document().archived_file_name = 'file.pdf'
fixture.detectChanges()
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.PDF
@@ -1659,8 +1661,8 @@ describe('DocumentDetailComponent', () => {
fixture.debugElement.query(By.css('pdf-viewer-container'))
).not.toBeUndefined()
component.document.archived_file_name = undefined
component.document.mime_type = 'text/plain'
component.document().archived_file_name = undefined
component.document().mime_type = 'text/plain'
fixture.detectChanges()
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.Text
@@ -1669,7 +1671,7 @@ describe('DocumentDetailComponent', () => {
fixture.debugElement.query(By.css('div.preview-sticky'))
).not.toBeUndefined()
component.document.mime_type = 'image/jpeg'
component.document().mime_type = 'image/jpeg'
fixture.detectChanges()
expect(component.archiveContentRenderType).toEqual(
component.ContentRenderType.Image
@@ -1677,7 +1679,7 @@ describe('DocumentDetailComponent', () => {
expect(
fixture.debugElement.query(By.css('.preview-sticky img'))
).not.toBeUndefined()
;((component.document.mime_type =
;((component.document().mime_type =
'application/vnd.openxmlformats-officedocument.wordprocessingml.document'),
fixture.detectChanges())
expect(component.archiveContentRenderType).toEqual(
@@ -1694,7 +1696,7 @@ describe('DocumentDetailComponent', () => {
const closeSpy = jest.spyOn(openDocumentsService, 'closeDocument')
const errorSpy = jest.spyOn(toastService, 'showError')
initNormally()
component.selectedVersionId = 10
component.selectedVersionId.set(10)
component.editPdf()
expect(modal).not.toBeUndefined()
modal.componentInstance.documentID.set(doc.id)
@@ -1731,7 +1733,7 @@ describe('DocumentDetailComponent', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[0]))
initNormally()
component.selectedVersionId = 10
component.selectedVersionId.set(10)
component.password = 'secret'
component.removePassword()
const dialog =
@@ -1785,7 +1787,7 @@ describe('DocumentDetailComponent', () => {
req.error(new ErrorEvent('failed'))
expect(errorSpy).toHaveBeenCalled()
expect(component.networkActive).toBe(false)
expect(component.networkActive()).toBe(false)
expect(dialog.buttonsEnabled).toBe(true)
})
@@ -1859,7 +1861,7 @@ describe('DocumentDetailComponent', () => {
it('selectVersion should update preview and handle preview failures', () => {
const previewSpy = jest.spyOn(documentService, 'getPreviewUrl')
initNormally()
httpTestingController.expectOne(component.previewUrl).flush('preview')
httpTestingController.expectOne(component.previewUrl()).flush('preview')
previewSpy.mockReturnValueOnce('preview-version')
jest.spyOn(documentService, 'getThumbUrl').mockReturnValue('thumb-version')
@@ -1870,12 +1872,12 @@ describe('DocumentDetailComponent', () => {
component.selectVersion(10)
httpTestingController.expectOne('preview-version').flush('version text')
expect(component.previewUrl).toBe('preview-version')
expect(component.thumbUrl).toBe('thumb-version')
expect(component.previewText).toBe('version text')
expect(component.previewUrl()).toBe('preview-version')
expect(component.thumbUrl()).toBe('thumb-version')
expect(component.previewText()).toBe('version text')
expect(component.documentForm.get('content').value).toBe('version-content')
expect(component.pdfSource).toBe('preview-version')
expect(component.pdfPassword).toBeUndefined()
expect(component.pdfSource()).toBe('preview-version')
expect(component.pdfPassword()).toBeUndefined()
previewSpy.mockReturnValueOnce('preview-error')
component.selectVersion(11)
@@ -1883,12 +1885,14 @@ describe('DocumentDetailComponent', () => {
.expectOne('preview-error')
.error(new ErrorEvent('fail'))
expect(component.previewText).toContain('An error occurred loading content')
expect(component.previewText()).toContain(
'An error occurred loading content'
)
})
it('selectVersion should show toast if version content retrieval fails', () => {
initNormally()
httpTestingController.expectOne(component.previewUrl).flush('preview')
httpTestingController.expectOne(component.previewUrl()).flush('preview')
jest.spyOn(documentService, 'getPreviewUrl').mockReturnValue('preview-ok')
jest.spyOn(documentService, 'getThumbUrl').mockReturnValue('thumb-ok')
@@ -1921,8 +1925,8 @@ describe('DocumentDetailComponent', () => {
})
it('onVersionsUpdated should sync open document versions and save', () => {
component.documentId = doc.id
component.document = { ...doc, versions: [] } as Document
component.documentId.set(doc.id)
component.document.set({ ...doc, versions: [] } as Document)
const updatedVersions = [
{ id: doc.id, is_root: true },
{ id: 10, is_root: false },
@@ -1933,7 +1937,7 @@ describe('DocumentDetailComponent', () => {
component.onVersionsUpdated(updatedVersions)
expect(component.document.versions).toEqual(updatedVersions)
expect(component.document().versions).toEqual(updatedVersions)
expect(openDoc.versions).toEqual(updatedVersions)
expect(saveSpy).toHaveBeenCalled()
})
@@ -1968,7 +1972,7 @@ describe('DocumentDetailComponent', () => {
DocumentDetailComponent.prototype as any,
'tryRenderTiff'
)
const doc = Object.assign({}, component.document)
const doc = Object.assign({}, component.document())
doc.archived_file_name = null
doc.mime_type = 'image/tiff'
jest
@@ -1987,21 +1991,21 @@ describe('DocumentDetailComponent', () => {
it('should try to render tiff and show error if failed', () => {
initNormally()
// just the text request
httpTestingController.expectOne(component.previewUrl)
httpTestingController.expectOne(component.previewUrl())
// invalid tiff
component['tryRenderTiff']()
httpTestingController
.expectOne(component.previewUrl)
.expectOne(component.previewUrl())
.flush(new ArrayBuffer(100)) // arraybuffer
expect(component.tiffError).not.toBeUndefined()
expect(component.tiffError()).not.toBeUndefined()
// http error
component['tryRenderTiff']()
httpTestingController
.expectOne(component.previewUrl)
.expectOne(component.previewUrl())
.error(new ErrorEvent('failed'))
expect(component.tiffError).not.toBeUndefined()
expect(component.tiffError()).not.toBeUndefined()
})
it('should support download using share sheet on mobile, direct download otherwise', () => {
@@ -2040,7 +2044,7 @@ describe('DocumentDetailComponent', () => {
it('should include version in download and print only for non-latest selected version', () => {
initNormally()
component.document.versions = [
component.document().versions = [
{ id: doc.id, is_root: true },
{ id: 10, is_root: false },
] as any
@@ -2052,7 +2056,7 @@ describe('DocumentDetailComponent', () => {
.mockReturnValueOnce('download-non-latest')
.mockReturnValueOnce('print-non-latest')
component.selectedVersionId = 10
component.selectedVersionId.set(10)
component.download()
expect(getDownloadUrlSpy).toHaveBeenNthCalledWith(
1,
@@ -2071,7 +2075,7 @@ describe('DocumentDetailComponent', () => {
.expectOne('print-latest')
.error(new ProgressEvent('failed'))
component.selectedVersionId = doc.id
component.selectedVersionId.set(doc.id)
component.download()
expect(getDownloadUrlSpy).toHaveBeenNthCalledWith(
3,
@@ -2093,7 +2097,7 @@ describe('DocumentDetailComponent', () => {
it('should omit version in download and print when no version is selected', () => {
initNormally()
component.document.versions = [] as any
component.document().versions = [] as any
;(component as any).selectedVersionId = undefined
const getDownloadUrlSpy = jest
@@ -2130,7 +2134,7 @@ describe('DocumentDetailComponent', () => {
})
const downloadUrl = 'http://example.com/download'
component.documentId = 123
component.documentId.set(123)
jest.spyOn(documentService, 'getDownloadUrl').mockReturnValue(downloadUrl)
const createSpy = jest.spyOn(document, 'createElement')
@@ -2165,16 +2169,18 @@ describe('DocumentDetailComponent', () => {
it('should set previewText', () => {
initNormally()
const previewText = 'Hello world, this is a test'
httpTestingController.expectOne(component.previewUrl).flush(previewText)
expect(component.previewText).toEqual(previewText)
httpTestingController.expectOne(component.previewUrl()).flush(previewText)
expect(component.previewText()).toEqual(previewText)
})
it('should set previewText to error message if preview fails', () => {
initNormally()
httpTestingController
.expectOne(component.previewUrl)
.expectOne(component.previewUrl())
.flush('fail', { status: 500, statusText: 'Server Error' })
expect(component.previewText).toContain('An error occurred loading content')
expect(component.previewText()).toContain(
'An error occurred loading content'
)
})
it('should print document successfully', fakeAsync(() => {
File diff suppressed because it is too large Load Diff
@@ -38,7 +38,6 @@ import { environment } from 'src/environments/environment'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { FilterableDropdownComponent } from '../../common/filterable-dropdown/filterable-dropdown.component'
@@ -1339,7 +1338,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newTag),
},
@@ -1382,7 +1381,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newCorrespondent),
},
@@ -1431,7 +1430,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newDocumentType),
},
@@ -1477,7 +1476,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newStoragePath),
},
@@ -1531,7 +1530,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newCustomField),
},
@@ -266,7 +266,7 @@ export class BulkEditorComponent
overrideSelection?: DocumentSelectionQuery
) {
if (modal) {
modal.componentInstance.buttonsEnabled = false
this.setModalButtonsEnabled(modal, false)
}
this.documentService
.bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args)
@@ -283,7 +283,7 @@ export class BulkEditorComponent
options: { deleteOriginals?: boolean } = {}
) {
if (modal) {
modal.componentInstance.buttonsEnabled = false
this.setModalButtonsEnabled(modal, false)
}
request.pipe(first()).subscribe({
next: () => {
@@ -313,7 +313,7 @@ export class BulkEditorComponent
private handleOperationError(modal: NgbModalRef, error: any) {
if (modal) {
modal.componentInstance.buttonsEnabled = true
this.setModalButtonsEnabled(modal, true)
}
this.toastService.showError(
$localize`Error executing bulk operation`,
@@ -321,6 +321,15 @@ export class BulkEditorComponent
)
}
private setModalButtonsEnabled(modal: NgbModalRef, enabled: boolean) {
const buttonsEnabled = modal.componentInstance.buttonsEnabled
if (typeof buttonsEnabled?.set === 'function') {
buttonsEnabled.set(enabled)
} else {
modal.componentInstance.buttonsEnabled = enabled
}
}
private applySelectionData(
items: SelectionDataItem[],
selectionModel: FilterableDropdownSelectionModel
@@ -736,7 +745,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(TagEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -757,7 +766,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(CorrespondentEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -780,7 +789,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(DocumentTypeEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -801,7 +810,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(StoragePathEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -822,7 +831,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(CustomFieldEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -75,9 +75,9 @@ export class CustomFieldsComponent
editField(field: CustomField) {
const modal = this.modalService.open(CustomFieldEditDialogComponent)
modal.componentInstance.dialogMode = field
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(
field ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
modal.componentInstance.object = field
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
@@ -221,7 +221,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
const activeModal = this.modalService.open(this.editDialogComponent, {
backdrop: 'static',
})
activeModal.componentInstance.dialogMode = EditDialogMode.CREATE
activeModal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
activeModal.componentInstance.succeeded.subscribe(() => {
this.reloadData()
this.toastService.showInfo(
@@ -241,7 +241,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
backdrop: 'static',
})
activeModal.componentInstance.object = object
activeModal.componentInstance.dialogMode = EditDialogMode.EDIT
activeModal.componentInstance.dialogMode.set(EditDialogMode.EDIT)
activeModal.componentInstance.succeeded.subscribe(() => {
this.reloadData()
this.toastService.showInfo(
@@ -261,7 +261,7 @@ describe('MailComponent', () => {
const editDialog = modal.componentInstance as MailRuleEditDialogComponent
expect(editDialog.object.id).toBeNull()
expect(editDialog.object.name).toEqual(`${mailRules[0].name} (copy)`)
expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE)
expect(editDialog.dialogMode()).toEqual(EditDialogMode.CREATE)
})
it('should support delete mail rule, show error if needed', () => {
@@ -162,9 +162,9 @@ export class MailComponent
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.dialogMode = account
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(
account ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
modal.componentInstance.object = account
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
@@ -241,8 +241,9 @@ export class MailComponent
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.dialogMode =
modal.componentInstance.dialogMode.set(
rule && !forceCreate ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
modal.componentInstance.object = rule
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
@@ -183,7 +183,7 @@ describe('WorkflowsComponent', () => {
expect(modal).not.toBeUndefined()
const editDialog = modal.componentInstance as WorkflowEditDialogComponent
expect(editDialog.object.name).toEqual(workflows[0].name + ' (copy)')
expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE)
expect(editDialog.dialogMode()).toEqual(EditDialogMode.CREATE)
})
it('should null ids on copy', () => {
@@ -73,8 +73,9 @@ export class WorkflowsComponent
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.dialogMode =
modal.componentInstance.dialogMode.set(
workflow && !forceCreate ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
if (workflow) {
// quick "deep" clone so original doesn't get modified
const clone = Object.assign({}, workflow)