Chorehancement: update to Angular v22, 'zoneless' / 'reactive' (#13114)

This commit is contained in:
shamoon
2026-07-10 00:42:16 -07:00
committed by GitHub
parent f244442c65
commit 106b41a15c
213 changed files with 5363 additions and 5842 deletions
@@ -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,22 +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>
}
@if (!hasNext()) {
<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>
@@ -466,45 +465,49 @@
</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%">
<span>Preview is unavailable.</span>
</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%">
<span>Preview is unavailable.</span>
</object>
}
}
@if (requiresPassword) {
@@ -9,13 +9,7 @@ import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import {
ActivatedRoute,
@@ -279,6 +273,7 @@ describe('DocumentDetailComponent', () => {
}).compileComponents()
router = TestBed.inject(Router)
jest.spyOn(router, 'navigate').mockResolvedValue(true)
activatedRoute = TestBed.inject(ActivatedRoute)
openDocumentsService = TestBed.inject(OpenDocumentsService)
documentService = TestBed.inject(DocumentService)
@@ -286,7 +281,7 @@ describe('DocumentDetailComponent', () => {
toastService = TestBed.inject(ToastService)
documentListViewService = TestBed.inject(DocumentListViewService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = { id: 1 }
settingsService.currentUser.set({ id: 1 })
customFieldsService = TestBed.inject(CustomFieldsService)
deviceDetectorService = TestBed.inject(DeviceDetectorService)
fixture = TestBed.createComponent(DocumentDetailComponent)
@@ -295,6 +290,10 @@ describe('DocumentDetailComponent', () => {
component = fixture.componentInstance
})
afterEach(() => {
jest.useRealTimers()
})
function initNormally() {
jest
.spyOn(activatedRoute, 'paramMap', 'get')
@@ -325,10 +324,13 @@ 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(() => {
it('should switch from preview to details when pdf preview enters the DOM', () => {
jest.useFakeTimers()
component.nav = {
activeId: component.DocumentDetailNavIDs.Preview,
select: jest.fn(),
@@ -337,11 +339,11 @@ describe('DocumentDetailComponent', () => {
nativeElement: { offsetParent: {} },
}
tick()
jest.advanceTimersByTime(0)
expect(component.nav.select).toHaveBeenCalledWith(
component.DocumentDetailNavIDs.Details
)
}))
})
it('should forward title key up value to titleSubject', () => {
const subjectSpy = jest.spyOn(component.titleSubject, 'next')
@@ -375,28 +377,29 @@ describe('DocumentDetailComponent', () => {
})
})
it('should update title after debounce', fakeAsync(() => {
it('should update title after debounce', () => {
jest.useFakeTimers()
initNormally()
component.titleInput.value = 'Foo Bar'
component.titleSubject.next('Foo Bar')
tick(1000)
jest.advanceTimersByTime(1000)
expect(component.documentForm.get('title').value).toEqual('Foo Bar')
discardPeriodicTasks()
}))
})
it('should update title before doc change if was not updated via debounce', fakeAsync(() => {
it('should update title before doc change if was not updated via debounce', () => {
jest.useFakeTimers()
initNormally()
component.titleInput.value = 'Foo Bar'
component.titleInput.inputField.nativeElement.dispatchEvent(
new Event('change')
)
tick(1000)
jest.advanceTimersByTime(1000)
expect(component.documentForm.get('title').value).toEqual('Foo Bar')
}))
})
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 +426,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 +458,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 +504,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 +515,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 +537,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 +581,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 +600,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 +616,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 +631,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 +651,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 +672,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 +697,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 +727,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 +742,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 +757,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 +770,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 +788,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 +806,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 +824,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,6 +856,7 @@ describe('DocumentDetailComponent', () => {
.find((b) => b.nativeElement.textContent === 'Save & next')
).toBeUndefined()
nextSpy.mockReturnValue(true)
component.networkActive.set(true)
fixture.detectChanges()
expect(
fixture.debugElement
@@ -1014,7 +1018,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()
@@ -1029,45 +1033,44 @@ 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(() => {
it('should mark preview loaded after pdf loads', () => {
initNormally()
expect(component.previewLoaded).toBeFalsy()
expect(component.previewLoaded()).toBeFalsy()
component.pdfPreviewLoaded({ numPages: 1000 } as any)
expect(component.previewNumPages).toEqual(1000)
tick(300)
expect(component.previewLoaded).toBeTruthy()
}))
expect(component.previewNumPages()).toEqual(1000)
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', () => {
@@ -1088,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()
})
@@ -1187,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()
@@ -1196,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()
@@ -1205,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()
@@ -1223,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
@@ -1267,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
@@ -1287,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
@@ -1355,7 +1358,7 @@ describe('DocumentDetailComponent', () => {
it('should show custom field errors', () => {
initNormally()
component.error = {
component.error.set({
custom_fields: [
{},
{},
@@ -1363,7 +1366,7 @@ describe('DocumentDetailComponent', () => {
{},
{ non_field_errors: ['Enter a valid URL.'] },
],
}
})
expect(component.getCustomFieldError(2)).toEqual([
'This field may not be null.',
])
@@ -1393,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: [],
@@ -1421,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: [],
@@ -1474,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,
}
@@ -1493,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.'
)
@@ -1515,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()
@@ -1530,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()).toBeNull()
expect(component.pdfPassword()).toBeUndefined()
})
it('should close incoming update modal if one is open', () => {
@@ -1606,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',
})
@@ -1621,7 +1624,7 @@ describe('DocumentDetailComponent', () => {
.mockImplementation(() => {})
;(component as any).handleIncomingDocumentUpdated({
document_id: component.documentId,
document_id: component.documentId(),
modified: '2026-02-17T00:00:00Z',
})
@@ -1629,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(() => {})
@@ -1649,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
@@ -1658,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
@@ -1668,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
@@ -1676,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(
@@ -1693,11 +1696,11 @@ 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 = doc.id
expect(modal.componentInstance.versionID).toBe(10)
modal.componentInstance.documentID.set(doc.id)
expect(modal.componentInstance.versionID()).toBe(10)
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: false }]
modal.componentInstance.confirm()
let req = httpTestingController.expectOne(
@@ -1715,7 +1718,7 @@ describe('DocumentDetailComponent', () => {
expect(errorSpy).toHaveBeenCalled()
component.editPdf()
modal.componentInstance.documentID = doc.id
modal.componentInstance.documentID.set(doc.id)
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: true }]
modal.componentInstance.deleteOriginal = true
modal.componentInstance.confirm()
@@ -1730,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 =
@@ -1784,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)
})
@@ -1858,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')
@@ -1869,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)
@@ -1882,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')
@@ -1920,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 },
@@ -1932,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()
})
@@ -1967,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
@@ -1986,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', () => {
@@ -2039,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
@@ -2051,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,
@@ -2070,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,
@@ -2092,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
@@ -2129,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')
@@ -2164,19 +2169,22 @@ 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(() => {
it('should print document successfully', () => {
jest.useFakeTimers()
initNormally()
const appendChildSpy = jest
@@ -2217,8 +2225,6 @@ describe('DocumentDetailComponent', () => {
)
req.flush(blob)
tick()
expect(createElementSpy).toHaveBeenCalledWith('iframe')
expect(appendChildSpy).toHaveBeenCalledWith(mockIframe)
expect(createObjectURLSpy).toHaveBeenCalledWith(blob)
@@ -2235,7 +2241,7 @@ describe('DocumentDetailComponent', () => {
if (mockIframe.onload) {
mockIframe.onload({} as any)
}
tick()
jest.advanceTimersByTime(0)
expect(mockContentWindow.focus).toHaveBeenCalled()
expect(mockContentWindow.print).toHaveBeenCalled()
@@ -2248,8 +2254,6 @@ describe('DocumentDetailComponent', () => {
mockContentWindow.onafterprint(new Event('afterprint'))
}
tick(500)
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
@@ -2258,7 +2262,7 @@ describe('DocumentDetailComponent', () => {
removeChildSpy.mockRestore()
createObjectURLSpy.mockRestore()
revokeObjectURLSpy.mockRestore()
}))
})
it('should show error toast if print document fails', () => {
initNormally()
@@ -2295,76 +2299,71 @@ describe('DocumentDetailComponent', () => {
]
iframePrintErrorCases.forEach(({ description, thrownError, expectToast }) => {
it(
description,
fakeAsync(() => {
initNormally()
it(description, () => {
jest.useFakeTimers()
initNormally()
const appendChildSpy = jest
.spyOn(document.body, 'appendChild')
.mockImplementation((node: Node) => node)
const removeChildSpy = jest
.spyOn(document.body, 'removeChild')
.mockImplementation((node: Node) => node)
const createObjectURLSpy = jest
.spyOn(URL, 'createObjectURL')
.mockReturnValue('blob:mock-url')
const revokeObjectURLSpy = jest
.spyOn(URL, 'revokeObjectURL')
.mockImplementation(() => {})
const appendChildSpy = jest
.spyOn(document.body, 'appendChild')
.mockImplementation((node: Node) => node)
const removeChildSpy = jest
.spyOn(document.body, 'removeChild')
.mockImplementation((node: Node) => node)
const createObjectURLSpy = jest
.spyOn(URL, 'createObjectURL')
.mockReturnValue('blob:mock-url')
const revokeObjectURLSpy = jest
.spyOn(URL, 'revokeObjectURL')
.mockImplementation(() => {})
const toastSpy = jest.spyOn(toastService, 'showError')
const toastSpy = jest.spyOn(toastService, 'showError')
const mockContentWindow = {
focus: jest.fn().mockImplementation(() => {
throw thrownError
}),
print: jest.fn(),
onafterprint: null,
}
const mockContentWindow = {
focus: jest.fn().mockImplementation(() => {
throw thrownError
}),
print: jest.fn(),
onafterprint: null,
}
const mockIframe: any = {
style: {},
src: '',
onload: null,
contentWindow: mockContentWindow,
}
const mockIframe: any = {
style: {},
src: '',
onload: null,
contentWindow: mockContentWindow,
}
const createElementSpy = jest
.spyOn(document, 'createElement')
.mockReturnValue(mockIframe as any)
const createElementSpy = jest
.spyOn(document, 'createElement')
.mockReturnValue(mockIframe as any)
const blob = new Blob(['test'], { type: 'application/pdf' })
component.printDocument()
const blob = new Blob(['test'], { type: 'application/pdf' })
component.printDocument()
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/${doc.id}/download/`
)
req.flush(blob)
const req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/${doc.id}/download/`
)
req.flush(blob)
tick()
if (mockIframe.onload) {
mockIframe.onload(new Event('load'))
}
if (mockIframe.onload) {
mockIframe.onload(new Event('load'))
}
jest.advanceTimersByTime(200)
tick()
tick(200)
if (expectToast) {
expect(toastSpy).toHaveBeenCalled()
} else {
expect(toastSpy).not.toHaveBeenCalled()
}
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
if (expectToast) {
expect(toastSpy).toHaveBeenCalled()
} else {
expect(toastSpy).not.toHaveBeenCalled()
}
expect(removeChildSpy).toHaveBeenCalledWith(mockIframe)
expect(revokeObjectURLSpy).toHaveBeenCalledWith('blob:mock-url')
createElementSpy.mockRestore()
appendChildSpy.mockRestore()
removeChildSpy.mockRestore()
createObjectURLSpy.mockRestore()
revokeObjectURLSpy.mockRestore()
})
)
createElementSpy.mockRestore()
appendChildSpy.mockRestore()
removeChildSpy.mockRestore()
createObjectURLSpy.mockRestore()
revokeObjectURLSpy.mockRestore()
})
})
})
File diff suppressed because it is too large Load Diff
@@ -1,17 +1,17 @@
@if (loading) {
@if (loading()) {
<div class="d-flex">
<output class="spinner-border spinner-border-sm fw-normal" role="status"></output>
</div>
} @else {
<ul class="list-group">
@if (entries.length === 0) {
@if (entries().length === 0) {
<li class="list-group-item">
<div class="d-flex justify-content-center">
<span class="fst-italic" i18n>No entries found.</span>
</div>
</li>
} @else {
@for (entry of entries; track entry.id) {
@for (entry of entries(); track entry.id) {
<li class="list-group-item">
<div class="d-flex justify-content-between align-items-center">
<ng-template #timestamp>
@@ -1,5 +1,5 @@
import { AsyncPipe, KeyValuePipe, TitleCasePipe } from '@angular/common'
import { Component, Input, OnInit, inject } from '@angular/core'
import { Component, Input, OnInit, inject, signal } from '@angular/core'
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Observable, first, map, of, shareReplay } from 'rxjs'
@@ -44,8 +44,8 @@ export class DocumentHistoryComponent implements OnInit {
}
}
public loading: boolean = true
public entries: AuditLogEntry[] = []
readonly loading = signal(true)
readonly entries = signal<AuditLogEntry[]>([])
private readonly prettyNameCache = new Map<string, Observable<string>>()
@@ -55,10 +55,10 @@ export class DocumentHistoryComponent implements OnInit {
private loadHistory(): void {
if (this._documentId) {
this.loading = true
this.loading.set(true)
this.documentService.getHistory(this._documentId).subscribe((entries) => {
this.entries = entries
this.loading = false
this.entries.set(entries)
this.loading.set(false)
})
}
}