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,7 +1,7 @@
<div [class.pe-none]="fileIsOver">
<div [class.pe-none]="fileIsOver()">
<ng-content select="[content]"></ng-content>
</div>
<div class="global-dropzone-overlay position-fixed top-0 start-0 bottom-0 end-0 text-center pe-none" [class.active]="fileIsOver && !hidden">
<div class="global-dropzone-overlay position-fixed top-0 start-0 bottom-0 end-0 text-center pe-none" [class.active]="fileIsOver() && !hidden()">
<h2 class="pe-none position-absolute top-50 start-50 translate-middle" i18n>Drop files to begin upload</h2>
</div>
@@ -1,13 +1,6 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
discardPeriodicTasks,
fakeAsync,
flush,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { PermissionsService } from 'src/app/services/permissions.service'
import { SettingsService } from 'src/app/services/settings.service'
@@ -24,8 +17,18 @@ describe('FileDropComponent', () => {
let settingsService: SettingsService
let uploadDocumentsService: UploadDocumentsService
beforeEach(() => {
TestBed.configureTestingModule({
const advanceTimers = (ms: number) => {
jest.advanceTimersByTime(ms)
fixture.detectChanges()
}
const flushPromises = async () => {
await Promise.resolve()
await Promise.resolve()
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FileDropComponent, ToastsComponent],
providers: [
provideHttpClient(withInterceptorsFromDi()),
@@ -43,6 +46,10 @@ describe('FileDropComponent', () => {
fixture.detectChanges()
})
afterEach(() => {
jest.useRealTimers()
})
it('should enable drag-drop if user has permissions', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.dragDropEnabled).toBeTruthy()
@@ -53,22 +60,21 @@ describe('FileDropComponent', () => {
expect(component.dragDropEnabled).toBeFalsy()
})
it('should disable drag-drop if disabled in settings', fakeAsync(() => {
it('should disable drag-drop if disabled in settings', () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
settingsService.globalDropzoneEnabled = false
settingsService.globalDropzoneEnabled.set(false)
expect(component.dragDropEnabled).toBeFalsy()
component.onDragOver(new Event('dragover') as DragEvent)
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeFalsy()
advanceTimers(1)
expect(component.fileIsOver()).toBeFalsy()
const dropzone = fixture.debugElement.query(
By.css('.global-dropzone-overlay')
)
expect(dropzone.classes['active']).toBeFalsy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
advanceTimers(700)
// drop
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
const dragEvent = new Event('drop')
@@ -79,22 +85,21 @@ describe('FileDropComponent', () => {
},
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
advanceTimers(3000)
expect(uploadSpy).not.toHaveBeenCalled()
}))
})
it('should support drag drop, initiate upload', fakeAsync(() => {
it('should support drag drop, initiate upload', () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver).toBeFalsy()
expect(component.fileIsOver()).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
advanceTimers(1)
expect(component.fileIsOver()).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
advanceTimers(700)
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
@@ -113,24 +118,22 @@ describe('FileDropComponent', () => {
],
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
advanceTimers(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
})
it('should support drag drop, initiate upload with webkitGetAsEntry', fakeAsync(() => {
it('should support drag drop, initiate upload with webkitGetAsEntry', async () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver).toBeFalsy()
expect(component.fileIsOver()).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
advanceTimers(1)
expect(component.fileIsOver()).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
advanceTimers(700)
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
@@ -154,13 +157,14 @@ describe('FileDropComponent', () => {
files: [],
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
await flushPromises()
advanceTimers(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
})
it('should show an error on traverseFileTree error', fakeAsync(() => {
it('should show an error on traverseFileTree error', async () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
const toastSpy = jest.spyOn(toastService, 'showError')
const traverseSpy = jest
@@ -190,28 +194,25 @@ describe('FileDropComponent', () => {
component.onDrop(event)
tick() // flush microtasks (e.g., Promise.reject)
await flushPromises()
expect(traverseSpy).toHaveBeenCalled()
expect(toastSpy).toHaveBeenCalledWith(
$localize`Failed to read dropped items: Error traversing file tree`
)
})
discardPeriodicTasks()
}))
it('should support drag drop, initiate upload without DataTransfer API support', fakeAsync(() => {
it('should support drag drop, initiate upload without DataTransfer API support', () => {
jest.useFakeTimers()
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
expect(component.fileIsOver).toBeFalsy()
expect(component.fileIsOver()).toBeFalsy()
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
fixture.detectChanges()
expect(component.fileIsOver).toBeTruthy()
advanceTimers(1)
expect(component.fileIsOver()).toBeTruthy()
component.onDragLeave(new Event('dragleave') as DragEvent)
tick(700)
fixture.detectChanges()
advanceTimers(700)
// drop
const toastSpy = jest.spyOn(toastService, 'show')
const uploadSpy = jest.spyOn(uploadDocumentsService, 'uploadFile')
@@ -225,11 +226,10 @@ describe('FileDropComponent', () => {
files: [file],
}
component.onDrop(dragEvent as DragEvent)
tick(3000)
advanceTimers(3000)
expect(toastSpy).toHaveBeenCalled()
expect(uploadSpy).toHaveBeenCalled()
discardPeriodicTasks()
}))
})
it('should resolve a single file when entry isFile', () => {
const mockFile = new File(['data'], 'test.txt', { type: 'text/plain' })
@@ -295,47 +295,47 @@ describe('FileDropComponent', () => {
})
})
it('should ignore events if disabled', fakeAsync(() => {
settingsService.globalDropzoneEnabled = false
expect(settingsService.globalDropzoneActive).toBeFalsy()
it('should ignore events if disabled', () => {
settingsService.globalDropzoneEnabled.set(false)
expect(settingsService.globalDropzoneActive()).toBeFalsy()
component.onDragOver(new Event('dragover') as DragEvent)
expect(settingsService.globalDropzoneActive).toBeFalsy()
settingsService.globalDropzoneActive = true
expect(settingsService.globalDropzoneActive()).toBeFalsy()
settingsService.globalDropzoneActive.set(true)
component.onDragLeave(new Event('dragleave') as DragEvent)
expect(settingsService.globalDropzoneActive).toBeTruthy()
expect(settingsService.globalDropzoneActive()).toBeTruthy()
component.onDrop(new Event('drop') as DragEvent)
expect(settingsService.globalDropzoneActive).toBeTruthy()
}))
expect(settingsService.globalDropzoneActive()).toBeTruthy()
})
it('should hide if app loses focus', fakeAsync(() => {
it('should hide if app loses focus', () => {
jest.useFakeTimers()
const leaveSpy = jest.spyOn(component, 'onDragLeave')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
settingsService.globalDropzoneEnabled = true
settingsService.globalDropzoneEnabled.set(true)
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
expect(component.hidden).toBeFalsy()
expect(component.fileIsOver).toBeTruthy()
advanceTimers(1)
expect(component.hidden()).toBeFalsy()
expect(component.fileIsOver()).toBeTruthy()
jest.spyOn(document, 'hidden', 'get').mockReturnValue(true)
component.onVisibilityChange()
expect(leaveSpy).toHaveBeenCalled()
flush()
}))
})
it('should hide on window blur', fakeAsync(() => {
it('should hide on window blur', () => {
jest.useFakeTimers()
const leaveSpy = jest.spyOn(component, 'onDragLeave')
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
settingsService.globalDropzoneEnabled = true
settingsService.globalDropzoneEnabled.set(true)
const overEvent = new Event('dragover') as DragEvent
;(overEvent as any).dataTransfer = { types: ['Files'] }
component.onDragOver(overEvent)
tick(1)
expect(component.hidden).toBeFalsy()
expect(component.fileIsOver).toBeTruthy()
advanceTimers(1)
expect(component.hidden()).toBeFalsy()
expect(component.fileIsOver()).toBeTruthy()
jest.spyOn(document, 'hidden', 'get').mockReturnValue(true)
component.onWindowBlur()
expect(leaveSpy).toHaveBeenCalled()
flush()
}))
})
})
@@ -1,4 +1,4 @@
import { Component, HostListener, inject } from '@angular/core'
import { Component, HostListener, inject, signal } from '@angular/core'
import {
PermissionAction,
PermissionsService,
@@ -21,12 +21,12 @@ export class FileDropComponent {
private permissionsService = inject(PermissionsService)
private fileLeaveTimeoutID: any
fileIsOver: boolean = false
hidden: boolean = true
fileIsOver = signal<boolean>(false)
hidden = signal<boolean>(true)
public get dragDropEnabled(): boolean {
return (
this.settings.globalDropzoneEnabled &&
this.settings.globalDropzoneEnabled() &&
this.permissionsService.currentUserCan(
PermissionAction.Add,
PermissionType.Document
@@ -39,12 +39,12 @@ export class FileDropComponent {
return
event.preventDefault()
event.stopImmediatePropagation()
this.settings.globalDropzoneActive = true
this.settings.globalDropzoneActive.set(true)
// allows transition
setTimeout(() => {
this.fileIsOver = true
this.fileIsOver.set(true)
}, 1)
this.hidden = false
this.hidden.set(false)
// stop fileLeave timeout
clearTimeout(this.fileLeaveTimeoutID)
}
@@ -56,15 +56,15 @@ export class FileDropComponent {
if (!this.dragDropEnabled) return
event?.preventDefault()
event?.stopImmediatePropagation()
this.settings.globalDropzoneActive = false
this.settings.globalDropzoneActive.set(false)
const ms = immediate ? 0 : 500
this.fileLeaveTimeoutID = setTimeout(() => {
this.fileIsOver = false
this.fileIsOver.set(false)
// await transition completed
setTimeout(() => {
this.hidden = true
this.hidden.set(true)
}, 150)
}, ms)
}