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,21 +1,21 @@
<div class="col p-2 h-100 fade" [class.show]="show">
<div class="card h-100 shadow-sm document-card" [class.placeholder-glow]="!document" [class.card-selected]="selected" (mouseleave)="mouseLeaveCard()">
<div class="col p-2 h-100 fade" [class.show]="show()">
<div class="card h-100 shadow-sm document-card" [class.placeholder-glow]="!document()" [class.card-selected]="selected()" (mouseleave)="mouseLeaveCard()">
<div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
@if (document) {
@if (document()) {
<img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
<div class="border-end border-bottom bg-light py-1 px-2 document-card-check">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="smallCardCheck{{document.id}}" [checked]="selected" (click)="this.toggleSelected.emit($event)">
<label class="form-check-label" for="smallCardCheck{{document.id}}"></label>
<input type="checkbox" class="form-check-input" id="smallCardCheck{{document().id}}" [checked]="selected()" (click)="this.toggleSelected.emit($event)">
<label class="form-check-label" for="smallCardCheck{{document().id}}"></label>
</div>
</div>
} @else {
<div class="placeholder bg-secondary w-100 card-img doc-img"></div>
}
@if (document && displayFields?.includes(DisplayField.TAGS)) {
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6" [class.tags-no-wrap]="document.tags.length > 3">
@if (document() && displayFields().includes(DisplayField.TAGS)) {
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6" [class.tags-no-wrap]="document().tags.length > 3">
@for (tagID of tagIDs; track tagID) {
<pngx-tag [tagID]="tagID" (click)="clickTag.emit(tagID);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
}
@@ -28,23 +28,23 @@
}
</div>
@if (document && displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
<a routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
@if (document() && displayFields().includes(DisplayField.NOTES) && notesEnabled && document().notes.length) {
<a routerLink="/documents/{{document().id}}/notes" class="document-card-notes py-2 px-1">
<span class="badge rounded-pill bg-light border text-primary">
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
{{document.notes.length}}</span>
{{document().notes.length}}</span>
</a>
}
<div class="card-body bg-light p-2">
<p class="card-text">
@if (document) {
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{document.correspondent | correspondentName | async}}</a>
@if (displayFields.includes(DisplayField.TITLE)) {:}
@if (document()) {
@if (displayFields().includes(DisplayField.CORRESPONDENT) && document().correspondent) {
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document().correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{document().correspondent | correspondentName | async}}</a>
@if (displayFields().includes(DisplayField.TITLE)) {:}
}
@if (displayFields.includes(DisplayField.TITLE)) {
{{document.title | documentTitle}}
@if (displayFields().includes(DisplayField.TITLE)) {
{{document().title | documentTitle}}
}
} @else {
<div class="placeholder bg-secondary w-100"></div>
@@ -54,82 +54,82 @@
</div>
<div class="card-footer pt-0 pb-2 px-2">
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
@if (document) {
@if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
@if (document()) {
@if (displayFields().includes(DisplayField.DOCUMENT_TYPE) && document().document_type) {
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
(click)="clickDocumentType.emit(document().document_type);$event.stopPropagation()">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="file-earmark"></i-bs>
<small>{{document.document_type | documentTypeName | async}}</small>
<small>{{document().document_type | documentTypeName | async}}</small>
</button>
}
@if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
@if (displayFields().includes(DisplayField.STORAGE_PATH) && document().storage_path) {
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
(click)="clickStoragePath.emit(document().storage_path);$event.stopPropagation()">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="folder"></i-bs>
<small>{{document.storage_path | storagePathName | async}}</small>
<small>{{document().storage_path | storagePathName | async}}</small>
</button>
}
@if (displayFields.includes(DisplayField.CREATED)) {
@if (displayFields().includes(DisplayField.CREATED)) {
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<ng-template #dateCreatedTooltip>
<div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span>
<span i18n>Created: {{ document().created | customDate }}</span>
<span i18n>Added: {{ document().added | customDate }}</span>
<span i18n>Modified: {{ document().modified | customDate }}</span>
</div>
</ng-template>
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateCreatedTooltip">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
<small>{{document.created | customDate:'mediumDate'}}</small>
<small>{{document().created | customDate:'mediumDate'}}</small>
</div>
</div>
}
@if (displayFields.includes(DisplayField.ADDED)) {
@if (displayFields().includes(DisplayField.ADDED)) {
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<ng-template #dateAddedTooltip>
<div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span>
<span i18n>Created: {{ document().created | customDate }}</span>
<span i18n>Added: {{ document().added | customDate }}</span>
<span i18n>Modified: {{ document().modified | customDate }}</span>
</div>
</ng-template>
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateAddedTooltip">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
<small>{{document.added | customDate:'mediumDate'}}</small>
<small>{{document().added | customDate:'mediumDate'}}</small>
</div>
</div>
}
@if (displayFields.includes(DisplayField.PAGE_COUNT) && document.page_count) {
@if (displayFields().includes(DisplayField.PAGE_COUNT) && document().page_count) {
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<div class="ps-0 p-1" placement="top">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="files"></i-bs>
<small i18n>{document.page_count, plural, =1 {1 page} other {{{document.page_count}} pages}}</small>
<small i18n>{document().page_count, plural, =1 {1 page} other {{{document().page_count}} pages}}</small>
</div>
</div>
}
@if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
@if (displayFields().includes(DisplayField.ASN) && document().archive_serial_number | isNumber) {
<div class="ps-0 p-1">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="upc-scan"></i-bs>
<small>#{{document.archive_serial_number}}</small>
<small>#{{document().archive_serial_number}}</small>
</div>
}
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
@if (displayFields().includes(DisplayField.OWNER) && document().owner && document().owner !== settingsService.currentUser().id) {
<div class="ps-0 p-1">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="person-fill-lock"></i-bs>
<small>{{document.owner | username | async}}</small>
<small>{{document().owner | username | async}}</small>
</div>
}
@if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
@if (displayFields().includes(DisplayField.SHARED) && document().is_shared_by_requester) {
<div class="ps-0 p-1">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="people-fill"></i-bs>
<small i18n>Shared</small>
</div>
}
@for (field of document.custom_fields; track field.field) {
@if (displayFields.includes(DisplayField.CUSTOM_FIELD + field.field)) {
@for (field of document().custom_fields; track field.field) {
@if (displayFields().includes(DisplayField.CUSTOM_FIELD + field.field)) {
<div class="ps-0 p-1 d-flex align-items-center overflow-hidden">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="ui-radios"></i-bs>
<small><pngx-custom-field-display [document]="document" [fieldId]="field.field" showNameIfEmpty="true"></pngx-custom-field-display></small>
<small><pngx-custom-field-display [document]="document()" [fieldId]="field.field" showNameIfEmpty="true"></pngx-custom-field-display></small>
</div>
}
}
@@ -139,11 +139,11 @@
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group w-100">
@if (document) {
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Open" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
@if (document()) {
<a routerLink="/documents/{{document().id}}" class="btn btn-sm btn-outline-secondary" title="Open" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
<i-bs name="file-earmark-richtext"></i-bs>
</a>
<pngx-preview-popup [document]="document" #popupPreview>
<pngx-preview-popup [document]="document()" #popupPreview>
<i-bs name="eye"></i-bs>
</pngx-preview-popup>
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title (click)="$event.stopPropagation()">
@@ -2,6 +2,26 @@
font-size: 90%;
}
.fade.show {
animation: pngx-entry-fade 160ms ease-out;
}
@keyframes pngx-entry-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.fade.show {
animation: none;
}
}
.doc-img {
object-fit: cover;
object-position: top left;
@@ -46,16 +46,15 @@ describe('DocumentCardSmallComponent', () => {
fixture = TestBed.createComponent(DocumentCardSmallComponent)
component = fixture.componentInstance
component.document = Object.assign({}, doc)
fixture.componentRef.setInput('document', Object.assign({}, doc))
fixture.detectChanges()
jest.useFakeTimers()
})
it('should show the card', () => {
expect(component.show).toBeFalsy()
expect(component.show()).toBeTruthy()
component.ngAfterViewInit()
jest.advanceTimersByTime(100)
expect(component.show).toBeTruthy()
expect(component.show()).toBeTruthy()
})
it('should display page count', () => {
@@ -67,7 +66,10 @@ describe('DocumentCardSmallComponent', () => {
expect(
fixture.debugElement.queryAll(By.directive(TagComponent))
).toHaveLength(5)
component.document.tags = [1, 2]
fixture.componentRef.setInput('document', {
...component.document(),
tags: [1, 2],
})
fixture.detectChanges()
expect(
fixture.debugElement.queryAll(By.directive(TagComponent))
@@ -75,7 +77,10 @@ describe('DocumentCardSmallComponent', () => {
})
it('should increase limit tags to 6 if no notes', () => {
component.document.notes = []
fixture.componentRef.setInput('document', {
...component.document(),
notes: [],
})
fixture.detectChanges()
expect(
fixture.debugElement.queryAll(By.directive(TagComponent))
@@ -85,7 +90,10 @@ describe('DocumentCardSmallComponent', () => {
it('should clear hidden tag counter when tag count falls below the limit', () => {
expect(component.moreTags).toEqual(3)
component.document.tags = [1, 2, 3, 4, 5, 6]
fixture.componentRef.setInput('document', {
...component.document(),
tags: [1, 2, 3, 4, 5, 6],
})
fixture.detectChanges()
expect(component.moreTags).toBeNull()
@@ -3,10 +3,10 @@ import {
AfterViewInit,
Component,
EventEmitter,
Input,
Output,
ViewChild,
inject,
input,
} from '@angular/core'
import { RouterModule } from '@angular/router'
import {
@@ -14,8 +14,6 @@ import {
NgbTooltipModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { delay } from 'rxjs/operators'
import {
DEFAULT_DISPLAY_FIELDS,
DisplayField,
@@ -66,21 +64,17 @@ export class DocumentCardSmallComponent
{
private documentService = inject(DocumentService)
settingsService = inject(SettingsService)
readonly selected = input(false)
readonly document = input<Document>(undefined)
readonly displayFields = input<string[]>(
DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
)
DisplayField = DisplayField
@Input()
selected = false
@Output()
toggleSelected = new EventEmitter()
@Input()
document: Document
@Input()
displayFields: string[] = DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
@Output()
dblClickDocument = new EventEmitter()
@@ -96,16 +90,18 @@ export class DocumentCardSmallComponent
@Output()
clickStoragePath = new EventEmitter<number>()
moreTags: number = null
get moreTags(): number {
const document = this.document()
const limit = document?.notes.length > 0 ? 6 : 7
return document?.tags.length > limit
? document.tags.length - (limit - 1)
: null
}
@ViewChild('popupPreview') popupPreview: PreviewPopupComponent
ngAfterViewInit(): void {
of(true)
.pipe(delay(50))
.subscribe(() => {
this.show = true
})
this.show.set(true)
}
getIsThumbInverted() {
@@ -113,21 +109,20 @@ export class DocumentCardSmallComponent
}
getThumbUrl() {
return this.documentService.getThumbUrl(this.document.id)
return this.documentService.getThumbUrl(this.document().id)
}
getDownloadUrl() {
return this.documentService.getDownloadUrl(this.document.id)
return this.documentService.getDownloadUrl(this.document().id)
}
get tagIDs() {
const limit = this.document.notes.length > 0 ? 6 : 7
if (this.document.tags.length > limit) {
this.moreTags = this.document.tags.length - (limit - 1)
return this.document.tags.slice(0, limit - 1)
const document = this.document()
const limit = document.notes.length > 0 ? 6 : 7
if (document.tags.length > limit) {
return document.tags.slice(0, limit - 1)
} else {
this.moreTags = null
return this.document.tags
return document.tags
}
}