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
@@ -7,7 +7,7 @@
>
</pngx-page-header>
@if (canViewUsers && users) {
@if (canViewUsers && users()) {
<h4 class="d-flex">
<ng-container i18n>Users</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editUser()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.User }">
@@ -23,7 +23,7 @@
<div class="col" i18n>Actions</div>
</div>
</li>
@for (user of users; track user) {
@for (user of users(); track user) {
<li class="list-group-item">
<div class="row">
<div class="col d-flex align-items-center" [class.opacity-50]="!user.is_active"><button class="btn btn-link p-0 text-start" type="button" (click)="editUser(user)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.User)">{{user.username}}</button></div>
@@ -45,7 +45,7 @@
</ul>
}
@if (canViewGroups && groups) {
@if (canViewGroups && groups()) {
<h4 class="mt-4 d-flex">
<ng-container i18n>Groups</ng-container>
<button type="button" class="btn btn-sm btn-outline-primary ms-4" (click)="editGroup()" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Group }">
@@ -61,7 +61,7 @@
<div class="col" i18n>Actions</div>
</div>
</li>
@for (group of groups; track group) {
@for (group of groups(); track group) {
<li class="list-group-item">
<div class="row">
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editGroup(group)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Group)">{{group.name}}</button></div>
@@ -80,13 +80,13 @@
</div>
</li>
}
@if (groups.length === 0) {
@if (groups().length === 0) {
<li class="list-group-item" i18n>No groups defined</li>
}
</ul>
}
@if ((canViewUsers && !users) || (canViewGroups && !groups)) {
@if ((canViewUsers && !users()) || (canViewGroups && !groups())) {
<div>
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
<div class="visually-hidden" i18n>Loading...</div>
@@ -1,12 +1,7 @@
import { DatePipe } from '@angular/common'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import {
ComponentFixture,
TestBed,
fakeAsync,
tick,
} from '@angular/core/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of, throwError } from 'rxjs'
@@ -57,7 +52,7 @@ describe('UsersAndGroupsComponent', () => {
}).compileComponents()
fixture = TestBed.createComponent(UsersAndGroupsComponent)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = users[0]
settingsService.currentUser.set(users[0])
userService = TestBed.inject(UserService)
modalService = TestBed.inject(NgbModal)
toastService = TestBed.inject(ToastService)
@@ -109,7 +104,7 @@ describe('UsersAndGroupsComponent', () => {
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
editDialog.failed.emit()
expect(toastErrorSpy).toHaveBeenCalled()
settingsService.currentUser = users[1] // simulate logged in as different user
settingsService.currentUser.set(users[1]) // simulate logged in as different user
editDialog.succeeded.emit(users[0])
expect(toastInfoSpy).toHaveBeenCalledWith(
`Saved user "${users[0].username}".`
@@ -138,7 +133,8 @@ describe('UsersAndGroupsComponent', () => {
expect(toastInfoSpy).toHaveBeenCalledWith('Deleted user "user1"')
})
it('should logout current user if password changed, after delay', fakeAsync(() => {
it('should logout current user if password changed, after delay', () => {
jest.useFakeTimers()
completeSetup()
let modal: NgbModalRef
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
@@ -148,14 +144,15 @@ describe('UsersAndGroupsComponent', () => {
.mockImplementation(() => {})
const editDialog = modal.componentInstance as UserEditDialogComponent
editDialog.passwordIsSet = true
settingsService.currentUser = users[0] // simulate logged in as same user
settingsService.currentUser.set(users[0]) // simulate logged in as same user
editDialog.succeeded.emit(users[0])
fixture.detectChanges()
tick(2600)
jest.advanceTimersByTime(2600)
expect(navSpy).toHaveBeenCalledWith(
`${window.location.origin}/accounts/logout/?next=/accounts/login/?next=/`
)
}))
jest.useRealTimers()
})
it('should support edit / create group, show error if needed', () => {
completeSetup()
@@ -1,4 +1,4 @@
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, first, takeUntil } from 'rxjs'
@@ -43,8 +43,8 @@ export class UsersAndGroupsComponent
permissionsService = inject(PermissionsService)
private settings = inject(SettingsService)
users: User[]
groups: Group[]
readonly users = signal<User[]>(null)
readonly groups = signal<Group[]>(null)
unsubscribeNotifier: Subject<any> = new Subject()
@@ -69,7 +69,7 @@ export class UsersAndGroupsComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (r) => {
this.users = r.results
this.users.set(r.results)
},
error: (e) => {
this.toastService.showError($localize`Error retrieving users`, e)
@@ -83,7 +83,7 @@ export class UsersAndGroupsComponent
.pipe(first(), takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (r) => {
this.groups = r.results
this.groups.set(r.results)
},
error: (e) => {
this.toastService.showError($localize`Error retrieving groups`, e)
@@ -101,15 +101,15 @@ export class UsersAndGroupsComponent
backdrop: 'static',
size: 'xl',
})
modal.componentInstance.dialogMode = user
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(
user ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
modal.componentInstance.object = user
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newUser: User) => {
if (
newUser.id === this.settings.currentUser.id &&
newUser.id === this.settings.currentUser().id &&
(modal.componentInstance as UserEditDialogComponent).passwordIsSet
) {
this.toastService.showInfo(
@@ -125,7 +125,7 @@ export class UsersAndGroupsComponent
$localize`Saved user "${newUser.username}".`
)
this.usersService.listAll().subscribe((r) => {
this.users = r.results
this.users.set(r.results)
})
}
})
@@ -152,7 +152,7 @@ export class UsersAndGroupsComponent
modal.close()
this.toastService.showInfo($localize`Deleted user "${user.username}"`)
this.usersService.listAll().subscribe((r) => {
this.users = r.results
this.users.set(r.results)
})
},
error: (e) => {
@@ -170,16 +170,16 @@ export class UsersAndGroupsComponent
backdrop: 'static',
size: 'lg',
})
modal.componentInstance.dialogMode = group
? EditDialogMode.EDIT
: EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(
group ? EditDialogMode.EDIT : EditDialogMode.CREATE
)
modal.componentInstance.object = group
modal.componentInstance.succeeded
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((newGroup) => {
this.toastService.showInfo($localize`Saved group "${newGroup.name}".`)
this.groupsService.listAll().subscribe((r) => {
this.groups = r.results
this.groups.set(r.results)
})
})
modal.componentInstance.failed
@@ -205,7 +205,7 @@ export class UsersAndGroupsComponent
modal.close()
this.toastService.showInfo($localize`Deleted group "${group.name}"`)
this.groupsService.listAll().subscribe((r) => {
this.groups = r.results
this.groups.set(r.results)
})
},
error: (e) => {
@@ -219,6 +219,6 @@ export class UsersAndGroupsComponent
}
getGroupName(id: number): string {
return this.groups?.find((g) => g.id === id)?.name ?? ''
return this.groups()?.find((g) => g.id === id)?.name ?? ''
}
}