mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-07-12 06:56:15 +00:00
Chorehancement: update to Angular v22, 'zoneless' / 'reactive' (#13114)
This commit is contained in:
@@ -59,10 +59,10 @@
|
||||
<div [ngbNavOutlet]="nav" class="border-start border-end border-bottom p-3 mb-3 shadow-sm"></div>
|
||||
<div class="btn-toolbar" role="toolbar">
|
||||
<div class="btn-group me-2">
|
||||
<button type="button" (click)="discardChanges()" class="btn btn-outline-secondary" [disabled]="loading || (isDirty$ | async) === false" i18n>Cancel</button>
|
||||
<button type="button" (click)="discardChanges()" class="btn btn-outline-secondary" [disabled]="loading() || (isDirty$ | async) === false" i18n>Cancel</button>
|
||||
</div>
|
||||
<div class="btn-group">
|
||||
<button type="submit" class="btn btn-primary" [disabled]="loading || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>
|
||||
<button type="submit" class="btn btn-primary" [disabled]="loading() || !configForm.valid || (isDirty$ | async) === false" i18n>Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -97,11 +97,11 @@ export class ConfigComponent
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (config) => {
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
this.initialize(config)
|
||||
},
|
||||
error: (e) => {
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
this.toastService.showError($localize`Error retrieving config`, e)
|
||||
},
|
||||
})
|
||||
@@ -162,20 +162,20 @@ export class ConfigComponent
|
||||
}
|
||||
|
||||
public saveConfig() {
|
||||
this.loading = true
|
||||
this.loading.set(true)
|
||||
this.configService
|
||||
.saveConfig(this.configForm.value as PaperlessConfig)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier), first())
|
||||
.subscribe({
|
||||
next: (config) => {
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
this.initialize(config)
|
||||
this.store.next(config)
|
||||
this.settingsService.initializeSettings().subscribe()
|
||||
this.toastService.showInfo($localize`Configuration updated`)
|
||||
},
|
||||
error: (e) => {
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred updating configuration`,
|
||||
e
|
||||
@@ -189,20 +189,20 @@ export class ConfigComponent
|
||||
}
|
||||
|
||||
public uploadFile(file: File, key: string) {
|
||||
this.loading = true
|
||||
this.loading.set(true)
|
||||
this.configService
|
||||
.uploadFile(file, this.configForm.value['id'], key)
|
||||
.pipe(takeUntil(this.unsubscribeNotifier), first())
|
||||
.subscribe({
|
||||
next: (config) => {
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
this.initialize(config)
|
||||
this.store.next(config)
|
||||
this.settingsService.initializeSettings().subscribe()
|
||||
this.toastService.showInfo($localize`File successfully updated`)
|
||||
},
|
||||
error: (e) => {
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
this.toastService.showError(
|
||||
$localize`An error occurred uploading file`,
|
||||
e
|
||||
|
||||
@@ -11,30 +11,30 @@
|
||||
type="number"
|
||||
min="100"
|
||||
step="100"
|
||||
[(ngModel)]="limit"
|
||||
(ngModelChange)="onLimitChange($event)"
|
||||
[ngModel]="limit()"
|
||||
(ngModelChange)="limit.set($event); onLimitChange($event)"
|
||||
style="width: 100px;">
|
||||
<span class="input-group-text text-muted" i18n>lines</span>
|
||||
</div>
|
||||
<div class="form-check form-switch mt-1">
|
||||
<input class="form-check-input" type="checkbox" role="switch" [(ngModel)]="autoRefreshEnabled">
|
||||
<input class="form-check-input" type="checkbox" role="switch" [ngModel]="autoRefreshEnabled()" (ngModelChange)="autoRefreshEnabled.set($event)">
|
||||
<label class="form-check-label" for="autoRefreshSwitch" i18n>Auto refresh</label>
|
||||
</div>
|
||||
</div>
|
||||
</pngx-page-header>
|
||||
|
||||
<ul ngbNav #nav="ngbNav" [(activeId)]="activeLog" (activeIdChange)="reloadLogs()" class="nav-tabs">
|
||||
@for (logFile of logFiles; track logFile) {
|
||||
<ul ngbNav #nav="ngbNav" [activeId]="activeLog()" (activeIdChange)="activeLog.set($event); reloadLogs()" class="nav-tabs">
|
||||
@for (logFile of logFiles(); track logFile) {
|
||||
<li [ngbNavItem]="logFile">
|
||||
<a ngbNavLink>
|
||||
{{logFile}}.log
|
||||
</a>
|
||||
</li>
|
||||
}
|
||||
@if (loading || !logFiles.length) {
|
||||
@if (loading() || !logFiles().length) {
|
||||
<div class="ps-2 d-flex align-items-center">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
@if (!logFiles.length) {
|
||||
@if (!logFiles().length) {
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
}
|
||||
</div>
|
||||
@@ -42,13 +42,13 @@
|
||||
</ul>
|
||||
|
||||
<div #logContainer class="bg-dark text-light font-monospace log-container p-3" (scroll)="onScroll()">
|
||||
@if (loading && !logFiles.length) {
|
||||
@if (loading() && !logFiles().length) {
|
||||
<div>
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</div>
|
||||
} @else {
|
||||
@for (log of logs; track log) {
|
||||
@for (log of logs(); track log) {
|
||||
<p class="m-0 p-0" [ngClass]="'log-entry-' + log.level">{{log.message}}</p>
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-sm btn-secondary jump-to-bottom position-fixed bottom-0 end-0 m-5"
|
||||
[class.visible]="showJumpToBottom"
|
||||
[class.visible]="showJumpToBottom()"
|
||||
(click)="scrollToBottom()"
|
||||
>
|
||||
↓ <span i18n>Jump to bottom</span>
|
||||
|
||||
@@ -86,7 +86,7 @@ describe('LogsComponent', () => {
|
||||
throwError(() => new Error('error getting logs'))
|
||||
)
|
||||
component.reloadLogs()
|
||||
expect(component.logs).toHaveLength(0)
|
||||
expect(component.logs()).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should auto refresh, allow toggle', () => {
|
||||
@@ -97,7 +97,7 @@ describe('LogsComponent', () => {
|
||||
jest.advanceTimersByTime(6000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
|
||||
component.autoRefreshEnabled = false
|
||||
component.autoRefreshEnabled.set(false)
|
||||
jest.advanceTimersByTime(6000)
|
||||
expect(reloadSpy).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
@@ -112,9 +112,9 @@ describe('LogsComponent', () => {
|
||||
})
|
||||
|
||||
it('should update jump to bottom visibility on scroll', () => {
|
||||
component.showJumpToBottom = false
|
||||
component.showJumpToBottom.set(false)
|
||||
jest.spyOn(component as any, 'isNearBottom').mockReturnValue(false)
|
||||
component.onScroll()
|
||||
expect(component.showJumpToBottom).toBe(true)
|
||||
expect(component.showJumpToBottom()).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
OnInit,
|
||||
ViewChild,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
@@ -34,17 +35,17 @@ export class LogsComponent
|
||||
private logService = inject(LogService)
|
||||
private changedetectorRef = inject(ChangeDetectorRef)
|
||||
|
||||
public logs: Array<{ message: string; level: number }> = []
|
||||
readonly logs = signal<Array<{ message: string; level: number }>>([])
|
||||
|
||||
public logFiles: string[] = []
|
||||
readonly logFiles = signal<string[]>([])
|
||||
|
||||
public activeLog: string
|
||||
readonly activeLog = signal<string>(undefined)
|
||||
|
||||
public autoRefreshEnabled: boolean = true
|
||||
readonly autoRefreshEnabled = signal<boolean>(true)
|
||||
|
||||
public limit: number = 5000
|
||||
readonly limit = signal<number>(5000)
|
||||
|
||||
public showJumpToBottom = false
|
||||
readonly showJumpToBottom = signal<boolean>(false)
|
||||
|
||||
private readonly limitChange$ = new Subject<number>()
|
||||
|
||||
@@ -59,15 +60,15 @@ export class LogsComponent
|
||||
.list()
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((result) => {
|
||||
this.logFiles = result
|
||||
this.loading = false
|
||||
if (this.logFiles.length > 0) {
|
||||
this.activeLog = this.logFiles[0]
|
||||
this.logFiles.set(result)
|
||||
this.loading.set(false)
|
||||
if (this.logFiles().length > 0) {
|
||||
this.activeLog.set(this.logFiles()[0])
|
||||
this.reloadLogs()
|
||||
}
|
||||
timer(5000, 5000)
|
||||
.pipe(
|
||||
filter(() => this.autoRefreshEnabled),
|
||||
filter(() => this.autoRefreshEnabled()),
|
||||
takeUntil(this.unsubscribeNotifier)
|
||||
)
|
||||
.subscribe(() => {
|
||||
@@ -85,19 +86,19 @@ export class LogsComponent
|
||||
}
|
||||
|
||||
reloadLogs() {
|
||||
this.loading = true
|
||||
this.loading.set(true)
|
||||
const shouldStickToBottom = this.isNearBottom()
|
||||
this.logService
|
||||
.get(this.activeLog, this.limit)
|
||||
.get(this.activeLog(), this.limit())
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
const parsed = this.parseLogsWithLevel(result)
|
||||
const hasChanges =
|
||||
parsed.length !== this.logs.length ||
|
||||
parsed.length !== this.logs().length ||
|
||||
parsed.some((log, idx) => {
|
||||
const current = this.logs[idx]
|
||||
const current = this.logs()[idx]
|
||||
return (
|
||||
!current ||
|
||||
current.message !== log.message ||
|
||||
@@ -105,16 +106,16 @@ export class LogsComponent
|
||||
)
|
||||
})
|
||||
if (hasChanges) {
|
||||
this.logs = parsed
|
||||
this.logs.set(parsed)
|
||||
if (shouldStickToBottom) {
|
||||
this.scrollToBottom()
|
||||
}
|
||||
this.showJumpToBottom = !shouldStickToBottom
|
||||
this.showJumpToBottom.set(!shouldStickToBottom)
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.logs = []
|
||||
this.loading = false
|
||||
this.logs.set([])
|
||||
this.loading.set(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -149,7 +150,7 @@ export class LogsComponent
|
||||
}
|
||||
this.changedetectorRef.detectChanges()
|
||||
viewport.scrollTop = viewport.scrollHeight
|
||||
this.showJumpToBottom = false
|
||||
this.showJumpToBottom.set(false)
|
||||
}
|
||||
|
||||
private isNearBottom(): boolean {
|
||||
@@ -162,6 +163,6 @@ export class LogsComponent
|
||||
}
|
||||
|
||||
onScroll(): void {
|
||||
this.showJumpToBottom = !this.isNearBottom()
|
||||
this.showJumpToBottom.set(!this.isNearBottom())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
</button>
|
||||
@if (canViewSystemStatus) {
|
||||
<button class="btn btn-sm btn-outline-primary position-relative ms-md-5 me-1" (click)="showSystemStatus()"
|
||||
[disabled]="!systemStatus">
|
||||
@if (!systemStatus) {
|
||||
[disabled]="!systemStatus()">
|
||||
@if (!systemStatus()) {
|
||||
<div class="spinner-border spinner-border-sm me-2 h-75" role="status"></div>
|
||||
} @else {
|
||||
<i-bs class="me-2" name="card-checklist"></i-bs>
|
||||
@@ -313,7 +313,7 @@
|
||||
<span i18n>Default Owner</span>
|
||||
</div>
|
||||
<div class="col-md-5">
|
||||
<pngx-input-select [items]="users" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
|
||||
<pngx-input-select [items]="users()" bindLabel="username" formControlName="defaultPermsOwner" [allowNull]="true"></pngx-input-select>
|
||||
<small class="form-text text-muted text-end d-block mt-n2" i18n>Objects without an owner can be viewed and edited by all users</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -99,6 +99,13 @@ const status: SystemStatus = {
|
||||
llmindex_status: SystemStatusItemStatus.DISABLED,
|
||||
llmindex_last_modified: new Date().toISOString(),
|
||||
llmindex_error: null,
|
||||
summary: {
|
||||
days: 30,
|
||||
total_count: 12,
|
||||
pending_count: 1,
|
||||
success_count: 10,
|
||||
failure_count: 1,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -161,7 +168,7 @@ describe('SettingsComponent', () => {
|
||||
viewportScroller = TestBed.inject(ViewportScroller)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = users[0]
|
||||
settingsService.currentUser.set(users[0])
|
||||
userService = TestBed.inject(UserService)
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
@@ -202,7 +209,7 @@ describe('SettingsComponent', () => {
|
||||
fixture.detectChanges()
|
||||
}
|
||||
|
||||
it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', () => {
|
||||
it('should support tabbed settings & change URL, prevent navigation if dirty confirmation rejected', async () => {
|
||||
completeSetup()
|
||||
const navigateSpy = jest.spyOn(router, 'navigate')
|
||||
const tabButtons = fixture.debugElement.queryAll(By.directive(NgbNavLink))
|
||||
@@ -210,16 +217,19 @@ describe('SettingsComponent', () => {
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'documents'])
|
||||
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions'])
|
||||
await fixture.whenStable()
|
||||
|
||||
const initSpy = jest.spyOn(component, 'initialize')
|
||||
component.isDirty = true // mock dirty
|
||||
navigateSpy.mockResolvedValueOnce(false) // nav rejected cause dirty
|
||||
tabButtons[0].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
await fixture.whenStable()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'general'])
|
||||
expect(initSpy).not.toHaveBeenCalled()
|
||||
|
||||
navigateSpy.mockResolvedValueOnce(true) // nav accepted even though dirty
|
||||
tabButtons[2].nativeElement.dispatchEvent(new MouseEvent('click'))
|
||||
await fixture.whenStable()
|
||||
expect(navigateSpy).toHaveBeenCalledWith(['settings', 'permissions'])
|
||||
expect(initSpy).toHaveBeenCalled()
|
||||
})
|
||||
@@ -340,13 +350,13 @@ describe('SettingsComponent', () => {
|
||||
type === PermissionType.SystemMonitoring
|
||||
)
|
||||
completeSetup()
|
||||
expect(component['systemStatus']).toEqual(status) // private
|
||||
expect(component.systemStatus()).toEqual(status)
|
||||
expect(component.systemStatusHasErrors).toBeTruthy()
|
||||
// coverage
|
||||
component['systemStatus'].database.status = SystemStatusItemStatus.OK
|
||||
component['systemStatus'].tasks.redis_status = SystemStatusItemStatus.OK
|
||||
component['systemStatus'].tasks.celery_status = SystemStatusItemStatus.OK
|
||||
component['systemStatus'].tasks.sanity_check_status =
|
||||
component.systemStatus().database.status = SystemStatusItemStatus.OK
|
||||
component.systemStatus().tasks.redis_status = SystemStatusItemStatus.OK
|
||||
component.systemStatus().tasks.celery_status = SystemStatusItemStatus.OK
|
||||
component.systemStatus().tasks.sanity_check_status =
|
||||
SystemStatusItemStatus.OK
|
||||
expect(component.systemStatusHasErrors).toBeFalsy()
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
@@ -189,10 +190,10 @@ export class SettingsComponent
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
savePending: boolean = false
|
||||
|
||||
users: User[]
|
||||
groups: Group[]
|
||||
readonly users = signal<User[]>(undefined)
|
||||
readonly groups = signal<Group[]>(undefined)
|
||||
|
||||
public systemStatus: SystemStatus
|
||||
public readonly systemStatus = signal<SystemStatus>(undefined)
|
||||
|
||||
public readonly GlobalSearchType = GlobalSearchType
|
||||
|
||||
@@ -203,16 +204,18 @@ export class SettingsComponent
|
||||
public readonly documentDetailFieldOptions = documentDetailFieldOptions
|
||||
|
||||
get systemStatusHasErrors(): boolean {
|
||||
const status = this.systemStatus()
|
||||
if (!status) {
|
||||
return false
|
||||
}
|
||||
return (
|
||||
this.systemStatus.database.status === SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.tasks.redis_status === SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.tasks.celery_status === SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.tasks.index_status === SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.tasks.classifier_status ===
|
||||
SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.tasks.sanity_check_status ===
|
||||
SystemStatusItemStatus.ERROR ||
|
||||
this.systemStatus.websocket_connected === SystemStatusItemStatus.ERROR
|
||||
status.database.status === SystemStatusItemStatus.ERROR ||
|
||||
status.tasks.redis_status === SystemStatusItemStatus.ERROR ||
|
||||
status.tasks.celery_status === SystemStatusItemStatus.ERROR ||
|
||||
status.tasks.index_status === SystemStatusItemStatus.ERROR ||
|
||||
status.tasks.classifier_status === SystemStatusItemStatus.ERROR ||
|
||||
status.tasks.sanity_check_status === SystemStatusItemStatus.ERROR ||
|
||||
status.websocket_connected === SystemStatusItemStatus.ERROR
|
||||
)
|
||||
}
|
||||
|
||||
@@ -246,7 +249,7 @@ export class SettingsComponent
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: (r) => {
|
||||
this.users = r.results
|
||||
this.users.set(r.results)
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error retrieving users`, e)
|
||||
@@ -265,7 +268,7 @@ export class SettingsComponent
|
||||
.pipe(first())
|
||||
.subscribe({
|
||||
next: (r) => {
|
||||
this.groups = r.results
|
||||
this.groups.set(r.results)
|
||||
},
|
||||
error: (e) => {
|
||||
this.toastService.showError($localize`Error retrieving groups`, e)
|
||||
@@ -431,7 +434,7 @@ export class SettingsComponent
|
||||
|
||||
if (this.canViewSystemStatus) {
|
||||
this.systemStatusService.get().subscribe((status) => {
|
||||
this.systemStatus = status
|
||||
this.systemStatus.set(status)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -664,6 +667,6 @@ export class SettingsComponent
|
||||
size: 'xl',
|
||||
}
|
||||
)
|
||||
modal.componentInstance.status = this.systemStatus
|
||||
modal.componentInstance.status.set(this.systemStatus())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="visibleTasks.length === 0">
|
||||
<i-bs name="check2-all" class="me-1"></i-bs>{{dismissButtonText}}
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissAllTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="totalTasks === 0">
|
||||
<button class="btn btn-sm btn-outline-primary me-2" (click)="dismissAllTasks()" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.PaperlessTask }" [disabled]="totalTasks() === 0">
|
||||
<i-bs name="check2-all" class="me-1"></i-bs><ng-container i18n>Dismiss all</ng-container>
|
||||
</button>
|
||||
<div class="form-check form-switch mb-0 ms-2">
|
||||
@@ -107,7 +107,7 @@
|
||||
|
||||
<ngb-pagination
|
||||
[pageSize]="pageSize"
|
||||
[collectionSize]="totalTasks"
|
||||
[collectionSize]="totalTasks()"
|
||||
[page]="page"
|
||||
[maxSize]="5"
|
||||
[rotate]="true"
|
||||
|
||||
@@ -362,7 +362,7 @@ describe('TasksComponent', () => {
|
||||
)
|
||||
|
||||
req.flush({ count: 2, results: [tasks[0], tasks[1]] })
|
||||
expect(component.totalTasks).toBe(2)
|
||||
expect(component.totalTasks()).toBe(2)
|
||||
})
|
||||
|
||||
it('should apply task type and trigger source filters to the server-side task query', () => {
|
||||
@@ -435,8 +435,8 @@ describe('TasksComponent', () => {
|
||||
.flush(pageTwoTasks)
|
||||
|
||||
expect(component.page).toBe(2)
|
||||
expect(component.totalTasks).toBe(30)
|
||||
expect(component.pagedTasks).toEqual([tasks[0]])
|
||||
expect(component.totalTasks()).toBe(30)
|
||||
expect(component.pagedTasks()).toEqual([tasks[0]])
|
||||
})
|
||||
|
||||
it('should not replace section counts with current-page counts', () => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { JsonPipe, NgTemplateOutlet } from '@angular/common'
|
||||
import { Component, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { Component, inject, OnDestroy, OnInit, signal } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router, RouterLink } from '@angular/router'
|
||||
import {
|
||||
@@ -166,14 +166,14 @@ export class TasksComponent
|
||||
public autoRefreshEnabled: boolean = true
|
||||
public readonly pageSize = 25
|
||||
public page: number = 1
|
||||
public totalTasks: number = 0
|
||||
public sectionCounts: Record<TaskSection, number> = {
|
||||
readonly totalTasks = signal(0)
|
||||
readonly sectionCounts = signal<Record<TaskSection, number>>({
|
||||
[TaskSection.All]: 0,
|
||||
[TaskSection.NeedsAttention]: 0,
|
||||
[TaskSection.InProgress]: 0,
|
||||
[TaskSection.Completed]: 0,
|
||||
}
|
||||
public pagedTasks: PaperlessTask[] = []
|
||||
})
|
||||
readonly pagedTasks = signal<PaperlessTask[]>([])
|
||||
public selectedSection: TaskSection = TaskSection.All
|
||||
public selectedTaskType: PaperlessTaskType | null = null
|
||||
public selectedTriggerSource: PaperlessTaskTriggerSource | null = null
|
||||
@@ -346,7 +346,7 @@ export class TasksComponent
|
||||
backdrop: 'static',
|
||||
})
|
||||
modal.componentInstance.title = $localize`Confirm Dismiss All`
|
||||
modal.componentInstance.messageBold = $localize`Dismiss all ${this.totalTasks} tasks?`
|
||||
modal.componentInstance.messageBold = $localize`Dismiss all ${this.totalTasks()} tasks?`
|
||||
modal.componentInstance.btnClass = 'btn-warning'
|
||||
modal.componentInstance.btnCaption = $localize`Dismiss`
|
||||
modal.componentInstance.confirmClicked.pipe(first()).subscribe(() => {
|
||||
@@ -465,7 +465,7 @@ export class TasksComponent
|
||||
}
|
||||
|
||||
tasksForSection(section: TaskSection): PaperlessTask[] {
|
||||
let tasks = this.pagedTasks.filter((task) =>
|
||||
let tasks = this.pagedTasks().filter((task) =>
|
||||
this.taskBelongsToSection(task, section)
|
||||
)
|
||||
|
||||
@@ -477,7 +477,14 @@ export class TasksComponent
|
||||
}
|
||||
|
||||
sectionCount(section: TaskSection): number {
|
||||
return this.sectionCounts[section]
|
||||
return this.sectionCounts()[section]
|
||||
}
|
||||
|
||||
private setSectionCount(section: TaskSection, count: number) {
|
||||
this.sectionCounts.update((counts) => ({
|
||||
...counts,
|
||||
[section]: count,
|
||||
}))
|
||||
}
|
||||
|
||||
sectionShowsResults(section: TaskSection): boolean {
|
||||
@@ -652,7 +659,7 @@ export class TasksComponent
|
||||
? this.sections
|
||||
: [this.selectedSection]
|
||||
|
||||
return this.pagedTasks.filter(
|
||||
return this.pagedTasks().filter(
|
||||
(task) =>
|
||||
sections.some((section) => this.taskBelongsToSection(task, section)) &&
|
||||
this.taskMatchesFilters(task, { taskType, triggerSource })
|
||||
@@ -664,10 +671,12 @@ export class TasksComponent
|
||||
.statusCounts(this.getParamsForSection(TaskSection.All))
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe((counts) => {
|
||||
this.sectionCounts[TaskSection.All] = counts.all
|
||||
this.sectionCounts[TaskSection.NeedsAttention] = counts.needs_attention
|
||||
this.sectionCounts[TaskSection.InProgress] = counts.in_progress
|
||||
this.sectionCounts[TaskSection.Completed] = counts.completed
|
||||
this.sectionCounts.set({
|
||||
[TaskSection.All]: counts.all,
|
||||
[TaskSection.NeedsAttention]: counts.needs_attention,
|
||||
[TaskSection.InProgress]: counts.in_progress,
|
||||
[TaskSection.Completed]: counts.completed,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -723,7 +732,7 @@ export class TasksComponent
|
||||
|
||||
this.reloadSectionCounts()
|
||||
|
||||
this.loading = true
|
||||
this.loading.set(true)
|
||||
this.tasksService
|
||||
.list(
|
||||
this.page,
|
||||
@@ -733,24 +742,24 @@ export class TasksComponent
|
||||
.pipe(first(), takeUntil(this.unsubscribeNotifier))
|
||||
.subscribe({
|
||||
next: (result) => {
|
||||
this.pagedTasks = result.results
|
||||
this.totalTasks = result.count
|
||||
this.sectionCounts[TaskSection.All] = result.count
|
||||
this.pagedTasks.set(result.results)
|
||||
this.totalTasks.set(result.count)
|
||||
this.setSectionCount(TaskSection.All, result.count)
|
||||
if (this.selectedSection !== TaskSection.All) {
|
||||
this.sectionCounts[this.selectedSection] = result.count
|
||||
this.setSectionCount(this.selectedSection, result.count)
|
||||
}
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
if (
|
||||
this.page > 1 &&
|
||||
this.pagedTasks.length === 0 &&
|
||||
this.totalTasks > 0
|
||||
this.pagedTasks().length === 0 &&
|
||||
this.totalTasks() > 0
|
||||
) {
|
||||
this.page -= 1
|
||||
this.reloadPage()
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -4,22 +4,22 @@
|
||||
info="Manage trashed documents that are pending deletion."
|
||||
i18n-info
|
||||
infoLink="usage/#document-trash">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedDocuments.size === 0">
|
||||
<button class="btn btn-sm btn-outline-secondary" (click)="clearSelection()" [hidden]="selectedDocuments().size === 0">
|
||||
<i-bs name="x" class="me-1"></i-bs><ng-container i18n>Clear selection</ng-container>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="restoreAll(selectedDocuments)" [disabled]="selectedDocuments.size === 0">
|
||||
<button type="button" class="btn btn-sm btn-outline-primary" (click)="restoreAll(selectedDocuments())" [disabled]="selectedDocuments().size === 0">
|
||||
<i-bs name="arrow-counterclockwise" class="me-1"></i-bs><ng-container i18n>Restore selected</ng-container>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedDocuments)" [disabled]="selectedDocuments.size === 0">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash(selectedDocuments())" [disabled]="selectedDocuments().size === 0">
|
||||
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Delete selected</ng-container>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="documentsInTrash.length === 0">
|
||||
<button type="button" class="btn btn-sm btn-outline-danger" (click)="emptyTrash()" [disabled]="documentsInTrash().length === 0">
|
||||
<i-bs name="trash" class="me-1"></i-bs><ng-container i18n>Empty trash</ng-container>
|
||||
</button>
|
||||
</pngx-page-header>
|
||||
|
||||
<div class="row mb-3">
|
||||
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="totalDocuments" [(page)]="page" [maxSize]="5" (pageChange)="reload()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
<ngb-pagination class="col-auto" [pageSize]="25" [collectionSize]="totalDocuments()" [page]="page()" [maxSize]="5" (pageChange)="page.set($event); reload()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
</div>
|
||||
|
||||
<div class="card border table-responsive mb-3">
|
||||
@@ -28,7 +28,7 @@
|
||||
<tr>
|
||||
<th scope="col">
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="allToggled" [disabled]="documentsInTrash.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [ngModel]="allToggled()" (ngModelChange)="allToggled.set($event)" [disabled]="documentsInTrash().length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-objects"></label>
|
||||
</div>
|
||||
</th>
|
||||
@@ -38,7 +38,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (loading) {
|
||||
@if (loading()) {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
@@ -46,11 +46,11 @@
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@for (document of documentsInTrash; track document.id) {
|
||||
<tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()" class="data-row fade" [class.show]="show">
|
||||
@for (document of documentsInTrash(); track document.id) {
|
||||
<tr (click)="toggleSelected(document); $event.stopPropagation();" (mouseleave)="popupPreview.close()" class="data-row fade" [class.show]="show()">
|
||||
<td>
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments.has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input" id="{{document.id}}" [checked]="selectedDocuments().has(document.id)" (click)="toggleSelected(document); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="{{document.id}}"></label>
|
||||
</div>
|
||||
</td>
|
||||
@@ -88,16 +88,16 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (!loading) {
|
||||
@if (!loading()) {
|
||||
<div class="d-flex mb-2">
|
||||
<div>
|
||||
<ng-container i18n>{totalDocuments, plural, =1 {One document in trash} other {{{totalDocuments || 0}} total documents in trash}}</ng-container>
|
||||
@if (selectedDocuments.size > 0) {
|
||||
({{selectedDocuments.size}} selected)
|
||||
<ng-container i18n>{totalDocuments(), plural, =1 {One document in trash} other {{{totalDocuments() || 0}} total documents in trash}}</ng-container>
|
||||
@if (selectedDocuments().size > 0) {
|
||||
({{selectedDocuments().size}} selected)
|
||||
}
|
||||
</div>
|
||||
@if (documentsInTrash.length > 20) {
|
||||
<ngb-pagination class="ms-auto" [pageSize]="25" [collectionSize]="totalDocuments" [(page)]="page" [maxSize]="5" (pageChange)="reload()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
@if (documentsInTrash().length > 20) {
|
||||
<ngb-pagination class="ms-auto" [pageSize]="25" [collectionSize]="totalDocuments()" [page]="page()" [maxSize]="5" (pageChange)="page.set($event); reload()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -60,10 +60,15 @@ describe('TrashComponent', () => {
|
||||
modalService = TestBed.inject(NgbModal)
|
||||
toastService = TestBed.inject(ToastService)
|
||||
router = TestBed.inject(Router)
|
||||
jest.spyOn(router, 'navigate').mockResolvedValue(true)
|
||||
component = fixture.componentInstance
|
||||
fixture.detectChanges()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('should call correct service method on reload', () => {
|
||||
jest.useFakeTimers()
|
||||
const trashSpy = jest.spyOn(trashService, 'getTrash')
|
||||
@@ -77,7 +82,7 @@ describe('TrashComponent', () => {
|
||||
component.reload()
|
||||
jest.advanceTimersByTime(100)
|
||||
expect(trashSpy).toHaveBeenCalled()
|
||||
expect(component.documentsInTrash).toEqual(documentsInTrash)
|
||||
expect(component.documentsInTrash()).toEqual(documentsInTrash)
|
||||
})
|
||||
|
||||
it('should support delete document, show error if needed', () => {
|
||||
@@ -179,8 +184,8 @@ describe('TrashComponent', () => {
|
||||
})
|
||||
|
||||
it('should support toggle all items in view', () => {
|
||||
component.documentsInTrash = documentsInTrash
|
||||
expect(component.selectedDocuments.size).toEqual(0)
|
||||
component.documentsInTrash.set(documentsInTrash)
|
||||
expect(component.selectedDocuments().size).toEqual(0)
|
||||
const toggleAllSpy = jest.spyOn(component, 'toggleAll')
|
||||
const checkButton = fixture.debugElement.queryAll(
|
||||
By.css('input.form-check-input')
|
||||
@@ -189,21 +194,21 @@ describe('TrashComponent', () => {
|
||||
checkButton.nativeElement.checked = true
|
||||
checkButton.nativeElement.dispatchEvent(new Event('click'))
|
||||
expect(toggleAllSpy).toHaveBeenCalled()
|
||||
expect(component.selectedDocuments.size).toEqual(documentsInTrash.length)
|
||||
expect(component.selectedDocuments().size).toEqual(documentsInTrash.length)
|
||||
})
|
||||
|
||||
it('should support toggle item', () => {
|
||||
component.selectedDocuments = new Set([1])
|
||||
component.selectedDocuments.set(new Set([1]))
|
||||
component.toggleSelected(documentsInTrash[0])
|
||||
expect(component.selectedDocuments.size).toEqual(0)
|
||||
expect(component.selectedDocuments().size).toEqual(0)
|
||||
component.toggleSelected(documentsInTrash[0])
|
||||
expect(component.selectedDocuments.size).toEqual(1)
|
||||
expect(component.selectedDocuments().size).toEqual(1)
|
||||
})
|
||||
|
||||
it('should support clear selection', () => {
|
||||
component.selectedDocuments = new Set([1])
|
||||
component.selectedDocuments.set(new Set([1]))
|
||||
component.clearSelection()
|
||||
expect(component.selectedDocuments.size).toEqual(0)
|
||||
expect(component.selectedDocuments().size).toEqual(0)
|
||||
})
|
||||
|
||||
it('should correctly display days remaining', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, OnDestroy, inject } from '@angular/core'
|
||||
import { Component, OnDestroy, inject, signal } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
NgbPaginationModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { delay, takeUntil, tap } from 'rxjs'
|
||||
import { takeUntil, tap } from 'rxjs'
|
||||
import { Document } from 'src/app/data/document'
|
||||
import { SETTINGS_KEYS } from 'src/app/data/ui-settings'
|
||||
import { SettingsService } from 'src/app/services/settings.service'
|
||||
@@ -42,11 +42,11 @@ export class TrashComponent
|
||||
private settingsService = inject(SettingsService)
|
||||
private router = inject(Router)
|
||||
|
||||
public documentsInTrash: Document[] = []
|
||||
public selectedDocuments: Set<number> = new Set()
|
||||
public allToggled: boolean = false
|
||||
public page: number = 1
|
||||
public totalDocuments: number
|
||||
readonly documentsInTrash = signal<Document[]>([])
|
||||
readonly selectedDocuments = signal<Set<number>>(new Set())
|
||||
readonly allToggled = signal(false)
|
||||
readonly page = signal(1)
|
||||
readonly totalDocuments = signal<number>(undefined)
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
@@ -54,20 +54,19 @@ export class TrashComponent
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.loading = true
|
||||
this.loading.set(true)
|
||||
this.trashService
|
||||
.getTrash(this.page)
|
||||
.getTrash(this.page())
|
||||
.pipe(
|
||||
tap((r) => {
|
||||
this.documentsInTrash = r.results
|
||||
this.totalDocuments = r.count
|
||||
this.selectedDocuments.clear()
|
||||
this.loading = false
|
||||
}),
|
||||
delay(100)
|
||||
this.documentsInTrash.set(r.results)
|
||||
this.totalDocuments.set(r.count)
|
||||
this.selectedDocuments.set(new Set())
|
||||
this.loading.set(false)
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.show = true
|
||||
this.show.set(true)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -122,7 +121,7 @@ export class TrashComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Document(s) deleted`)
|
||||
this.allToggled = false
|
||||
this.allToggled.set(false)
|
||||
modal.close()
|
||||
this.reload()
|
||||
},
|
||||
@@ -165,7 +164,7 @@ export class TrashComponent
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.toastService.showInfo($localize`Document(s) restored`)
|
||||
this.allToggled = false
|
||||
this.allToggled.set(false)
|
||||
this.reload()
|
||||
},
|
||||
error: (err) => {
|
||||
@@ -179,24 +178,29 @@ export class TrashComponent
|
||||
|
||||
toggleAll(event: PointerEvent) {
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
this.selectedDocuments = new Set(this.documentsInTrash.map((t) => t.id))
|
||||
this.selectedDocuments.set(
|
||||
new Set(this.documentsInTrash().map((t) => t.id))
|
||||
)
|
||||
} else {
|
||||
this.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
toggleSelected(object: Document) {
|
||||
this.selectedDocuments.has(object.id)
|
||||
? this.selectedDocuments.delete(object.id)
|
||||
: this.selectedDocuments.add(object.id)
|
||||
const selectedDocuments = new Set(this.selectedDocuments())
|
||||
selectedDocuments.has(object.id)
|
||||
? selectedDocuments.delete(object.id)
|
||||
: selectedDocuments.add(object.id)
|
||||
this.selectedDocuments.set(selectedDocuments)
|
||||
}
|
||||
|
||||
clearSelection() {
|
||||
this.allToggled = false
|
||||
this.selectedDocuments.clear()
|
||||
this.allToggled.set(false)
|
||||
this.selectedDocuments.set(new Set())
|
||||
}
|
||||
|
||||
getDaysRemaining(document: Document): number {
|
||||
this.settingsService.trackChanges()
|
||||
const delay = this.settingsService.get(SETTINGS_KEYS.EMPTY_TRASH_DELAY)
|
||||
const diff = new Date().getTime() - new Date(document.deleted_at).getTime()
|
||||
const days = Math.ceil(diff / (1000 * 3600 * 24))
|
||||
|
||||
@@ -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 ?? ''
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user