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

This commit is contained in:
shamoon authored and GitHub committed 2026-07-10 00:42:16 -07:00
1 parent f244442c65
commit 106b41a15c
213 files changed
+5363 -5842

No files matched your search

@@ -4,27 +4,27 @@
</div>
<div class="modal-body">
@if (loading) {
@if (loading()) {
<div class="d-flex align-items-center gap-2">
<div class="spinner-border spinner-border-sm" role="status"></div>
<span i18n>Loading share link bundles…</span>
</div>
}
@if (!loading && error) {
@if (!loading() && error()) {
<div class="alert alert-danger mb-0" role="alert">
{{ error }}
{{ error() }}
</div>
}
@if (!loading && !error) {
@if (!loading() && !error()) {
<div class="d-flex justify-content-between align-items-center mb-2">
<p class="mb-0 text-muted small">
<ng-container i18n>Status updates every few seconds while bundles are being prepared.</ng-container>
</p>
</div>
@if (bundles.length === 0) {
@if (bundles().length === 0) {
<p class="mb-0 text-muted fst-italic" i18n>No share link bundles currently exist.</p>
}
@if (bundles.length > 0) {
@if (bundles().length > 0) {
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
@@ -39,7 +39,7 @@
</tr>
</thead>
<tbody>
@for (bundle of bundles; track bundle.id) {
@for (bundle of bundles(); track bundle.id) {
<tr>
<td>
<div>{{ bundle.created | date: 'short' }}</div>
@@ -113,10 +113,10 @@
title="Copy share link"
i18n-title
>
@if (copiedSlug === bundle.slug) {
@if (copiedSlug() === bundle.slug) {
<i-bs name="clipboard-check"></i-bs>
}
@if (copiedSlug !== bundle.slug) {
@if (copiedSlug() !== bundle.slug) {
<i-bs name="clipboard"></i-bs>
}
<span class="visually-hidden" i18n>Copy share link</span>
@@ -125,7 +125,7 @@
<button
type="button"
class="btn btn-outline-warning"
[disabled]="loading"
[disabled]="loading()"
(click)="retry(bundle)"
>
<i-bs name="arrow-clockwise"></i-bs>
@@ -134,7 +134,7 @@
}
<pngx-confirm-button
buttonClasses="btn btn-sm btn-outline-danger"
[disabled]="loading"
[disabled]="loading()"
(confirm)="delete(bundle)"
iconName="trash"
>
@@ -1,10 +1,5 @@
import { Clipboard } from '@angular/cdk/clipboard'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
@@ -70,6 +65,7 @@ describe('ShareLinkBundleManageDialogComponent', () => {
fixture.destroy()
environment.apiBaseUrl = originalApiBaseUrl
jest.clearAllMocks()
jest.useRealTimers()
})
const sampleBundle = (overrides: Partial<ShareLinkBundleSummary> = {}) =>
@@ -85,7 +81,8 @@ describe('ShareLinkBundleManageDialogComponent', () => {
...overrides,
}) as ShareLinkBundleSummary
it('loads bundles on init and polls periodically', fakeAsync(() => {
it('loads bundles on init and polls periodically', () => {
jest.useFakeTimers()
const bundles = [sampleBundle({ status: ShareLinkBundleStatus.Ready })]
service.listAllBundles.mockReset()
service.listAllBundles
@@ -93,38 +90,37 @@ describe('ShareLinkBundleManageDialogComponent', () => {
.mockReturnValue(of(bundles))
fixture.detectChanges()
tick()
expect(service.listAllBundles).toHaveBeenCalledTimes(1)
expect(component.bundles).toEqual(bundles)
expect(component.loading).toBe(false)
expect(component.error).toBeNull()
expect(component.bundles()).toEqual(bundles)
expect(component.loading()).toBe(false)
expect(component.error()).toBeNull()
tick(5000)
jest.advanceTimersByTime(5000)
expect(service.listAllBundles).toHaveBeenCalledTimes(2)
}))
})
it('handles errors when loading bundles', fakeAsync(() => {
it('handles errors when loading bundles', () => {
jest.useFakeTimers()
service.listAllBundles.mockReset()
service.listAllBundles
.mockReturnValueOnce(throwError(() => new Error('load fail')))
.mockReturnValue(of([]))
fixture.detectChanges()
tick()
expect(component.error).toContain('Failed to load share link bundles.')
expect(component.error()).toContain('Failed to load share link bundles.')
expect(toastService.showError).toHaveBeenCalled()
expect(component.loading).toBe(false)
expect(component.loading()).toBe(false)
tick(5000)
jest.advanceTimersByTime(5000)
expect(service.listAllBundles).toHaveBeenCalledTimes(2)
}))
})
it('copies bundle links when ready', fakeAsync(() => {
it('copies bundle links when ready', () => {
jest.useFakeTimers()
jest.spyOn(clipboard, 'copy').mockReturnValue(true)
fixture.detectChanges()
tick()
const readyBundle = sampleBundle({
slug: 'ready-slug',
@@ -135,103 +131,91 @@ describe('ShareLinkBundleManageDialogComponent', () => {
expect(clipboard.copy).toHaveBeenCalledWith(
component.getShareUrl(readyBundle)
)
expect(component.copiedSlug).toBe('ready-slug')
expect(component.copiedSlug()).toBe('ready-slug')
expect(toastService.showInfo).toHaveBeenCalled()
tick(3000)
expect(component.copiedSlug).toBeNull()
}))
jest.advanceTimersByTime(3000)
expect(component.copiedSlug()).toBeNull()
})
it('ignores copy requests for non-ready bundles', fakeAsync(() => {
it('ignores copy requests for non-ready bundles', () => {
const copySpy = jest.spyOn(clipboard, 'copy')
fixture.detectChanges()
tick()
component.copy(sampleBundle({ status: ShareLinkBundleStatus.Pending }))
expect(copySpy).not.toHaveBeenCalled()
}))
})
it('deletes bundles and refreshes list', fakeAsync(() => {
it('deletes bundles and refreshes list', () => {
service.listAllBundles.mockReturnValue(of([]))
service.delete.mockReturnValue(of(true))
fixture.detectChanges()
tick()
component.delete(sampleBundle())
tick()
expect(service.delete).toHaveBeenCalled()
expect(toastService.showInfo).toHaveBeenCalledWith(
expect.stringContaining('deleted.')
)
expect(service.listAllBundles).toHaveBeenCalledTimes(2)
expect(component.loading).toBe(false)
}))
expect(component.loading()).toBe(false)
})
it('handles delete errors gracefully', fakeAsync(() => {
it('handles delete errors gracefully', () => {
service.listAllBundles.mockReturnValue(of([]))
service.delete.mockReturnValue(throwError(() => new Error('delete fail')))
fixture.detectChanges()
tick()
component.delete(sampleBundle())
tick()
expect(toastService.showError).toHaveBeenCalled()
expect(component.loading).toBe(false)
}))
expect(component.loading()).toBe(false)
})
it('retries bundle build and replaces existing entry', fakeAsync(() => {
it('retries bundle build and replaces existing entry', () => {
service.listAllBundles.mockReturnValue(of([]))
const updated = sampleBundle({ status: ShareLinkBundleStatus.Ready })
service.rebuildBundle.mockReturnValue(of(updated))
fixture.detectChanges()
tick()
component.bundles = [sampleBundle()]
component.retry(component.bundles[0])
tick()
component.bundles.set([sampleBundle()])
component.retry(component.bundles()[0])
expect(service.rebuildBundle).toHaveBeenCalledWith(updated.id)
expect(component.bundles[0].status).toBe(ShareLinkBundleStatus.Ready)
expect(component.bundles()[0].status).toBe(ShareLinkBundleStatus.Ready)
expect(toastService.showInfo).toHaveBeenCalled()
}))
})
it('adds new bundle when retry returns unknown entry', fakeAsync(() => {
it('adds new bundle when retry returns unknown entry', () => {
service.listAllBundles.mockReturnValue(of([]))
service.rebuildBundle.mockReturnValue(
of(sampleBundle({ id: 99, slug: 'new-slug' }))
)
fixture.detectChanges()
tick()
component.bundles = [sampleBundle()]
component.bundles.set([sampleBundle()])
component.retry({ id: 99 } as ShareLinkBundleSummary)
tick()
expect(component.bundles.find((bundle) => bundle.id === 99)).toBeTruthy()
}))
expect(component.bundles().find((bundle) => bundle.id === 99)).toBeTruthy()
})
it('handles retry errors', fakeAsync(() => {
it('handles retry errors', () => {
service.listAllBundles.mockReturnValue(of([]))
service.rebuildBundle.mockReturnValue(throwError(() => new Error('fail')))
fixture.detectChanges()
tick()
component.retry(sampleBundle())
tick()
expect(toastService.showError).toHaveBeenCalled()
}))
})
it('maps helpers and closes dialog', fakeAsync(() => {
it('maps helpers and closes dialog', () => {
service.listAllBundles.mockReturnValue(of([]))
fixture.detectChanges()
tick()
expect(component.statusLabel(ShareLinkBundleStatus.Processing)).toContain(
'Processing'
@@ -247,5 +231,5 @@ describe('ShareLinkBundleManageDialogComponent', () => {
const closeSpy = jest.spyOn(activeModal, 'close')
component.close()
expect(closeSpy).toHaveBeenCalled()
}))
})
})
@@ -1,6 +1,6 @@
import { Clipboard } from '@angular/cdk/clipboard'
import { CommonModule } from '@angular/common'
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
import { NgbActiveModal, NgbPopoverModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, catchError, of, switchMap, takeUntil, timer } from 'rxjs'
@@ -40,10 +40,9 @@ export class ShareLinkBundleManageDialogComponent
private readonly clipboard = inject(Clipboard)
title = $localize`Share link bundles`
bundles: ShareLinkBundleSummary[] = []
error: string | null = null
copiedSlug: string | null = null
readonly bundles = signal<ShareLinkBundleSummary[]>([])
readonly error = signal<string | null>(null)
readonly copiedSlug = signal<string | null>(null)
readonly statuses = ShareLinkBundleStatus
readonly fileVersions = FileVersion
@@ -55,15 +54,15 @@ export class ShareLinkBundleManageDialogComponent
.pipe(
switchMap((silent) => {
if (!silent) {
this.loading = true
this.loading.set(true)
}
this.error = null
this.error.set(null)
return this.shareLinkBundleService.listAllBundles().pipe(
catchError((error) => {
if (!silent) {
this.loading = false
this.loading.set(false)
}
this.error = $localize`Failed to load share link bundles.`
this.error.set($localize`Failed to load share link bundles.`)
this.toastService.showError(
$localize`Error retrieving share link bundles.`,
error
@@ -76,10 +75,10 @@ export class ShareLinkBundleManageDialogComponent
)
.subscribe((results) => {
if (results) {
this.bundles = results
this.copiedSlug = null
this.bundles.set(results)
this.copiedSlug.set(null)
}
this.loading = false
this.loading.set(false)
})
this.triggerRefresh(false)
@@ -105,24 +104,24 @@ export class ShareLinkBundleManageDialogComponent
}
const success = this.clipboard.copy(this.getShareUrl(bundle))
if (success) {
this.copiedSlug = bundle.slug
this.copiedSlug.set(bundle.slug)
setTimeout(() => {
this.copiedSlug = null
this.copiedSlug.set(null)
}, 3000)
this.toastService.showInfo($localize`Share link copied to clipboard.`)
}
}
delete(bundle: ShareLinkBundleSummary): void {
this.error = null
this.loading = true
this.error.set(null)
this.loading.set(true)
this.shareLinkBundleService.delete(bundle).subscribe({
next: () => {
this.toastService.showInfo($localize`Share link bundle deleted.`)
this.triggerRefresh(false)
},
error: (e) => {
this.loading = false
this.loading.set(false)
this.toastService.showError(
$localize`Error deleting share link bundle.`,
e
@@ -132,7 +131,7 @@ export class ShareLinkBundleManageDialogComponent
}
retry(bundle: ShareLinkBundleSummary): void {
this.error = null
this.error.set(null)
this.shareLinkBundleService.rebuildBundle(bundle.id).subscribe({
next: (updated) => {
this.toastService.showInfo(
@@ -159,15 +158,16 @@ export class ShareLinkBundleManageDialogComponent
}
private replaceBundle(updated: ShareLinkBundleSummary): void {
const index = this.bundles.findIndex((bundle) => bundle.id === updated.id)
const bundles = this.bundles()
const index = bundles.findIndex((bundle) => bundle.id === updated.id)
if (index >= 0) {
this.bundles = [
...this.bundles.slice(0, index),
this.bundles.set([
...bundles.slice(0, index),
updated,
...this.bundles.slice(index + 1),
]
...bundles.slice(index + 1),
])
} else {
this.bundles = [updated, ...this.bundles]
this.bundles.set([updated, ...bundles])
}
}