mirror of
https://github.com/paperless-ngx/paperless-ngx.git
synced 2026-08-03 01:22:17 +00:00
Chorehancement: update to Angular v22, 'zoneless' / 'reactive' (#13114)
This commit is contained in:
+4
-4
@@ -8,16 +8,16 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (loading) {
|
||||
@if (loading()) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (field of fields; track field) {
|
||||
@for (field of fields(); track field) {
|
||||
<li class="list-group-item">
|
||||
<div class="row fade" [class.show]="show">
|
||||
<div class="row fade" [class.show]="show()">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editField(field)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.CustomField)">{{field.name}}</button></div>
|
||||
<div class="col d-flex align-items-center">{{getDataType(field)}}</div>
|
||||
<div class="col">
|
||||
@@ -64,7 +64,7 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (!loading && fields.length === 0) {
|
||||
@if (!loading() && fields().length === 0) {
|
||||
<li class="list-group-item" i18n>No fields defined.</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
+1
-1
@@ -94,7 +94,7 @@ describe('CustomFieldsComponent', () => {
|
||||
toastService = TestBed.inject(ToastService)
|
||||
listViewService = TestBed.inject(DocumentListViewService)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = { id: 0, username: 'test' }
|
||||
settingsService.currentUser.set({ id: 0, username: 'test' })
|
||||
|
||||
fixture = TestBed.createComponent(CustomFieldsComponent)
|
||||
component = fixture.componentInstance
|
||||
|
||||
+11
-11
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { Component, OnInit, inject, signal } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import {
|
||||
NgbDropdownModule,
|
||||
@@ -6,7 +6,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 { ConfirmDialogComponent } from 'src/app/components/common/confirm-dialog/confirm-dialog.component'
|
||||
import { CustomFieldEditDialogComponent } from 'src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
|
||||
import { EditDialogMode } from 'src/app/components/common/edit-dialog/edit-dialog.component'
|
||||
@@ -51,33 +51,33 @@ export class CustomFieldsComponent
|
||||
private readonly documentService = inject(DocumentService)
|
||||
private readonly savedViewService = inject(SavedViewService)
|
||||
|
||||
public fields: CustomField[] = []
|
||||
readonly fields = signal<CustomField[]>([])
|
||||
|
||||
ngOnInit() {
|
||||
this.reload()
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.loading.set(true)
|
||||
this.customFieldsService
|
||||
.listAll()
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((r) => {
|
||||
this.fields = r.results
|
||||
}),
|
||||
delay(100)
|
||||
this.fields.set(r.results)
|
||||
})
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.show = true
|
||||
this.loading = false
|
||||
this.show.set(true)
|
||||
this.loading.set(false)
|
||||
})
|
||||
}
|
||||
|
||||
editField(field: CustomField) {
|
||||
const modal = this.modalService.open(CustomFieldEditDialogComponent)
|
||||
modal.componentInstance.dialogMode = field
|
||||
? EditDialogMode.EDIT
|
||||
: EditDialogMode.CREATE
|
||||
modal.componentInstance.dialogMode.set(
|
||||
field ? EditDialogMode.EDIT : EditDialogMode.CREATE
|
||||
)
|
||||
modal.componentInstance.object = field
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
|
||||
+1
-1
@@ -59,7 +59,7 @@
|
||||
}
|
||||
</pngx-page-header>
|
||||
|
||||
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [(activeId)]="activeNavID" class="nav-underline">
|
||||
<ul ngbNav #nav="ngbNav" (navChange)="onNavChange($event)" [activeId]="activeNavID()" (activeIdChange)="activeNavID.set($event)" class="nav-underline">
|
||||
@for (section of visibleSections; track section.id) {
|
||||
<li [ngbNavItem]="section.id">
|
||||
<a ngbNavLink >
|
||||
|
||||
+6
-6
@@ -100,7 +100,7 @@ describe('DocumentAttributesComponent', () => {
|
||||
expect(router.navigate).toHaveBeenCalledWith(['attributes', 'tags'], {
|
||||
replaceUrl: true,
|
||||
})
|
||||
expect(component.activeNavID).toBe(1)
|
||||
expect(component.activeNavID()).toBe(1)
|
||||
})
|
||||
|
||||
it('should set active section from route param when valid', () => {
|
||||
@@ -116,7 +116,7 @@ describe('DocumentAttributesComponent', () => {
|
||||
fixture.detectChanges()
|
||||
paramMapSubject.next(convertToParamMap({ section: 'customfields' }))
|
||||
|
||||
expect(component.activeNavID).toBe(2)
|
||||
expect(component.activeNavID()).toBe(2)
|
||||
expect(router.navigate).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
@@ -124,10 +124,10 @@ describe('DocumentAttributesComponent', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
|
||||
fixture.detectChanges()
|
||||
component.activeNavID = 1
|
||||
component.activeNavID.set(1)
|
||||
paramMapSubject.next(convertToParamMap({ section: 'customfields' }))
|
||||
|
||||
expect(component.activeNavID).toBe(2)
|
||||
expect(component.activeNavID()).toBe(2)
|
||||
})
|
||||
|
||||
it('should redirect to dashboard when no sections are visible', () => {
|
||||
@@ -169,7 +169,7 @@ describe('DocumentAttributesComponent', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
expect(component.activeManagementList).toBeNull()
|
||||
|
||||
component.activeNavID = 1
|
||||
component.activeNavID.set(1)
|
||||
expect(component.activeSection.kind).toBe(
|
||||
DocumentAttributesSectionKind.ManagementList
|
||||
)
|
||||
@@ -180,7 +180,7 @@ describe('DocumentAttributesComponent', () => {
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
expect(component.activeCustomFields).toBeNull()
|
||||
|
||||
component.activeNavID = 2
|
||||
component.activeNavID.set(2)
|
||||
expect(component.activeSection.kind).toBe(
|
||||
DocumentAttributesSectionKind.CustomFields
|
||||
)
|
||||
|
||||
+17
-27
@@ -1,11 +1,10 @@
|
||||
import { NgComponentOutlet } from '@angular/common'
|
||||
import {
|
||||
AfterViewChecked,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
inject,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
signal,
|
||||
Type,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
@@ -70,13 +69,10 @@ interface DocumentAttributesSection {
|
||||
ClearableBadgeComponent,
|
||||
],
|
||||
})
|
||||
export class DocumentAttributesComponent
|
||||
implements OnInit, OnDestroy, AfterViewChecked
|
||||
{
|
||||
export class DocumentAttributesComponent implements OnInit, OnDestroy {
|
||||
private readonly permissionsService = inject(PermissionsService)
|
||||
private readonly activatedRoute = inject(ActivatedRoute)
|
||||
private readonly router = inject(Router)
|
||||
private readonly cdr = inject(ChangeDetectorRef)
|
||||
private readonly unsubscribeNotifier = new Subject<void>()
|
||||
|
||||
protected readonly PermissionAction = PermissionAction
|
||||
@@ -136,11 +132,12 @@ export class DocumentAttributesComponent
|
||||
]
|
||||
|
||||
@ViewChild('activeOutlet', { read: NgComponentOutlet })
|
||||
private readonly activeOutlet?: NgComponentOutlet
|
||||
set activeOutlet(outlet: NgComponentOutlet | undefined) {
|
||||
this.activeComponent.set(outlet?.componentInstance ?? null)
|
||||
}
|
||||
|
||||
private lastHeaderLoading: boolean
|
||||
|
||||
activeNavID: number = null
|
||||
readonly activeComponent = signal<unknown>(null)
|
||||
readonly activeNavID = signal<number>(null)
|
||||
|
||||
get visibleSections(): DocumentAttributesSection[] {
|
||||
return this.sections.filter((section) =>
|
||||
@@ -153,8 +150,9 @@ export class DocumentAttributesComponent
|
||||
|
||||
get activeSection(): DocumentAttributesSection | null {
|
||||
return (
|
||||
this.visibleSections.find((section) => section.id === this.activeNavID) ??
|
||||
null
|
||||
this.visibleSections.find(
|
||||
(section) => section.id === this.activeNavID()
|
||||
) ?? null
|
||||
)
|
||||
}
|
||||
|
||||
@@ -163,14 +161,14 @@ export class DocumentAttributesComponent
|
||||
this.activeSection?.kind !== DocumentAttributesSectionKind.ManagementList
|
||||
)
|
||||
return null
|
||||
const instance = this.activeOutlet?.componentInstance
|
||||
const instance = this.activeComponent()
|
||||
return instance instanceof ManagementListComponent ? instance : null
|
||||
}
|
||||
|
||||
get activeCustomFields(): CustomFieldsComponent | null {
|
||||
if (this.activeSection?.kind !== DocumentAttributesSectionKind.CustomFields)
|
||||
return null
|
||||
const instance = this.activeOutlet?.componentInstance
|
||||
const instance = this.activeComponent()
|
||||
return instance instanceof CustomFieldsComponent ? instance : null
|
||||
}
|
||||
|
||||
@@ -184,8 +182,8 @@ export class DocumentAttributesComponent
|
||||
|
||||
get activeHeaderLoading(): boolean {
|
||||
return (
|
||||
this.activeManagementList?.loading ??
|
||||
this.activeCustomFields?.loading ??
|
||||
this.activeManagementList?.loading() ??
|
||||
this.activeCustomFields?.loading() ??
|
||||
false
|
||||
)
|
||||
}
|
||||
@@ -203,13 +201,13 @@ export class DocumentAttributesComponent
|
||||
return
|
||||
}
|
||||
|
||||
if (this.activeNavID !== navIDFromSection) {
|
||||
this.activeNavID = navIDFromSection
|
||||
if (this.activeNavID() !== navIDFromSection) {
|
||||
this.activeNavID.set(navIDFromSection)
|
||||
}
|
||||
|
||||
if (!section || this.getNavIDForSection(section) == null) {
|
||||
this.router.navigate(
|
||||
['attributes', this.getSectionForNavID(this.activeNavID)],
|
||||
['attributes', this.getSectionForNavID(this.activeNavID())],
|
||||
{ replaceUrl: true }
|
||||
)
|
||||
}
|
||||
@@ -221,14 +219,6 @@ export class DocumentAttributesComponent
|
||||
this.unsubscribeNotifier.complete()
|
||||
}
|
||||
|
||||
ngAfterViewChecked(): void {
|
||||
const current = this.activeHeaderLoading
|
||||
if (this.lastHeaderLoading !== current) {
|
||||
this.lastHeaderLoading = current
|
||||
this.cdr.detectChanges()
|
||||
}
|
||||
}
|
||||
|
||||
onNavChange(navChangeEvent: NgbNavChangeEvent): void {
|
||||
const nextSection = this.getSectionForNavID(navChangeEvent.nextId)
|
||||
if (!nextSection) {
|
||||
|
||||
+14
-14
@@ -19,7 +19,7 @@
|
||||
</select>
|
||||
<span class="input-group-text text-muted d-none d-md-flex" i18n>per page</span>
|
||||
</div>
|
||||
<ngb-pagination [pageSize]="pageSize" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
<ngb-pagination [pageSize]="pageSize" [collectionSize]="collectionSize()" [page]="page()" [maxSize]="5" (pageChange)="page.set($event); reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -31,21 +31,21 @@
|
||||
<tr>
|
||||
<th>
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="togggleAll" [disabled]="data.length === 0" (change)="$event.target.checked ? selectPage() : clearSelection(); $event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [ngModel]="togggleAll()" [disabled]="data().length === 0" (change)="$event.target.checked ? selectPage() : clearSelection(); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-objects"></label>
|
||||
</div>
|
||||
</th>
|
||||
<th class="fw-normal" pngxSortable="name" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Name</th>
|
||||
<th class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Matching</th>
|
||||
<th class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)" i18n>Document count</th>
|
||||
<th class="fw-normal" pngxSortable="name" [currentSortField]="sortField()" [currentSortReverse]="sortReverse()" (sort)="onSort($event)" i18n>Name</th>
|
||||
<th class="fw-normal d-none d-sm-table-cell" pngxSortable="matching_algorithm" [currentSortField]="sortField()" [currentSortReverse]="sortReverse()" (sort)="onSort($event)" i18n>Matching</th>
|
||||
<th class="fw-normal" pngxSortable="document_count" [currentSortField]="sortField()" [currentSortReverse]="sortReverse()" (sort)="onSort($event)" i18n>Document count</th>
|
||||
@for (column of extraColumns; track column) {
|
||||
<th class="fw-normal" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }" pngxSortable="{{column.key}}" [currentSortField]="sortField" [currentSortReverse]="sortReverse" (sort)="onSort($event)">{{column.name}}</th>
|
||||
<th class="fw-normal" [ngClass]="{ 'd-none d-sm-table-cell' : column.hideOnMobile }" pngxSortable="{{column.key}}" [currentSortField]="sortField()" [currentSortReverse]="sortReverse()" (sort)="onSort($event)">{{column.name}}</th>
|
||||
}
|
||||
<th class="fw-normal" i18n>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@if (loading && data.length === 0) {
|
||||
@if (loading() && data().length === 0) {
|
||||
<tr>
|
||||
<td colspan="5">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
@@ -53,31 +53,31 @@
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@for (object of data; track object) {
|
||||
@for (object of data(); track object) {
|
||||
<ng-container [ngTemplateOutlet]="objectRow" [ngTemplateOutletContext]="{ object: object, depth: 0 }"></ng-container>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@if (!loading || data.length > 0) {
|
||||
@if (!loading() || data().length > 0) {
|
||||
<div class="d-flex mb-2">
|
||||
@if (displayCollectionSize > 0) {
|
||||
@if (displayCollectionSize() > 0) {
|
||||
<div>
|
||||
<ng-container i18n>{displayCollectionSize, plural, =1 {One {{typeName}}} other {{{displayCollectionSize || 0}} total {{typeNamePlural}}}}</ng-container>
|
||||
<ng-container i18n>{displayCollectionSize(), plural, =1 {One {{typeName}}} other {{{displayCollectionSize() || 0}} total {{typeNamePlural}}}}</ng-container>
|
||||
@if (hasSelection) {
|
||||
({{selectedCount}} selected)
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (collectionSize > 20) {
|
||||
<ngb-pagination class="ms-auto" [pageSize]="pageSize" [collectionSize]="collectionSize" [(page)]="page" [maxSize]="5" (pageChange)="reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
@if (collectionSize() > 20) {
|
||||
<ngb-pagination class="ms-auto" [pageSize]="pageSize" [collectionSize]="collectionSize()" [page]="page()" [maxSize]="5" (pageChange)="page.set($event); reloadData()" size="sm" aria-label="Pagination"></ngb-pagination>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-template #objectRow let-object="object" let-depth="depth">
|
||||
<tr (click)="toggleSelected(object); $event.stopPropagation();" class="data-row fade" [class.show]="show">
|
||||
<tr (click)="toggleSelected(object); $event.stopPropagation();" 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="{{typeName}}{{object.id}}" [checked]="selectedObjects.has(object.id)" (click)="toggleSelected(object); $event.stopPropagation();">
|
||||
|
||||
+41
-46
@@ -5,12 +5,7 @@ import {
|
||||
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 { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { By } from '@angular/platform-browser'
|
||||
import { RouterLinkWithHref } from '@angular/router'
|
||||
@@ -140,24 +135,26 @@ describe('ManagementListComponent', () => {
|
||||
|
||||
// These tests are shared among all management list components
|
||||
|
||||
it('should support filtering, clear on Esc key', fakeAsync(() => {
|
||||
it('should support filtering, clear on Esc key', () => {
|
||||
jest.useFakeTimers()
|
||||
const nameFilterInput = fixture.debugElement.query(By.css('input'))
|
||||
nameFilterInput.nativeElement.value = 'foo'
|
||||
// nameFilterInput.nativeElement.dispatchEvent(new Event('input'))
|
||||
component.nameFilter = 'foo' // subject normally triggered by ngModel
|
||||
tick(400) // debounce
|
||||
jest.advanceTimersByTime(400) // debounce
|
||||
fixture.detectChanges()
|
||||
expect(component.data).toEqual([tags[0]])
|
||||
expect(component.data()).toEqual([tags[0]])
|
||||
|
||||
nameFilterInput.nativeElement.dispatchEvent(
|
||||
new KeyboardEvent('keyup', { code: 'Escape' })
|
||||
)
|
||||
tick(400) // debounce
|
||||
jest.advanceTimersByTime(400) // debounce
|
||||
fixture.detectChanges()
|
||||
expect(component.nameFilter).toBeNull()
|
||||
expect(component.data).toEqual(tags)
|
||||
tick(100) // load
|
||||
}))
|
||||
expect(component.data()).toEqual(tags)
|
||||
jest.advanceTimersByTime(100) // load
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('should support create, show notification on error / success', () => {
|
||||
let modal: NgbModalRef
|
||||
@@ -230,7 +227,8 @@ describe('ManagementListComponent', () => {
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should use API count for pagination and nested ids for displayed total', fakeAsync(() => {
|
||||
it('should use API count for pagination and nested ids for displayed total', () => {
|
||||
jest.useFakeTimers()
|
||||
jest.spyOn(tagService, 'listFiltered').mockReturnValueOnce(
|
||||
of({
|
||||
count: 1,
|
||||
@@ -240,11 +238,12 @@ describe('ManagementListComponent', () => {
|
||||
)
|
||||
|
||||
component.reloadData()
|
||||
tick(100)
|
||||
jest.advanceTimersByTime(100)
|
||||
|
||||
expect(component.collectionSize).toBe(1)
|
||||
expect(component.displayCollectionSize).toBe(3)
|
||||
}))
|
||||
expect(component.collectionSize()).toBe(1)
|
||||
expect(component.displayCollectionSize()).toBe(3)
|
||||
jest.useRealTimers()
|
||||
})
|
||||
|
||||
it('should support quick filter for objects', () => {
|
||||
const expectedUrl = documentListViewService.getQuickFilterUrl([
|
||||
@@ -275,9 +274,9 @@ describe('ManagementListComponent', () => {
|
||||
})
|
||||
)
|
||||
)
|
||||
component.page = 2
|
||||
component.page.set(2)
|
||||
component.reloadData()
|
||||
expect(component.page).toEqual(1)
|
||||
expect(component.page()).toEqual(1)
|
||||
})
|
||||
|
||||
it('should support toggle select page in vew', () => {
|
||||
@@ -295,28 +294,28 @@ describe('ManagementListComponent', () => {
|
||||
|
||||
it('selectNone should clear selection and reset toggle flag', () => {
|
||||
component.selectedObjects = new Set([tags[0].id, tags[1].id])
|
||||
component.togggleAll = true
|
||||
component.togggleAll.set(true)
|
||||
|
||||
component.selectNone()
|
||||
|
||||
expect(component.selectedObjects.size).toBe(0)
|
||||
expect(component.togggleAll).toBe(false)
|
||||
expect(component.togggleAll()).toBe(false)
|
||||
})
|
||||
|
||||
it('selectPage should select current page items or clear selection', () => {
|
||||
component.selectPage()
|
||||
expect(component.selectedObjects).toEqual(new Set(tags.map((t) => t.id)))
|
||||
expect(component.togggleAll).toBe(true)
|
||||
expect(component.togggleAll()).toBe(true)
|
||||
|
||||
component.togggleAll = true
|
||||
component.togggleAll.set(true)
|
||||
component.clearSelection()
|
||||
expect(component.selectedObjects.size).toBe(0)
|
||||
expect(component.togggleAll).toBe(false)
|
||||
expect(component.togggleAll()).toBe(false)
|
||||
})
|
||||
|
||||
it('selectAll should activate all-selection mode', () => {
|
||||
;(tagService.listFiltered as jest.Mock).mockClear()
|
||||
component.collectionSize = tags.length
|
||||
component.collectionSize.set(tags.length)
|
||||
|
||||
component.selectAll()
|
||||
|
||||
@@ -325,41 +324,41 @@ describe('ManagementListComponent', () => {
|
||||
expect((component as any).allSelectionActive).toBe(true)
|
||||
expect(component.hasSelection).toBe(true)
|
||||
expect(component.selectedCount).toBe(tags.length)
|
||||
expect(component.togggleAll).toBe(true)
|
||||
expect(component.togggleAll()).toBe(true)
|
||||
})
|
||||
|
||||
it('selectAll should clear selection when collection size is zero', () => {
|
||||
component.selectedObjects = new Set([1])
|
||||
component.collectionSize = 0
|
||||
component.togggleAll = true
|
||||
component.collectionSize.set(0)
|
||||
component.togggleAll.set(true)
|
||||
|
||||
component.selectAll()
|
||||
|
||||
expect(component.selectedObjects.size).toBe(0)
|
||||
expect(component.togggleAll).toBe(false)
|
||||
expect(component.togggleAll()).toBe(false)
|
||||
})
|
||||
|
||||
it('toggleSelected should toggle object selection and update toggle state', () => {
|
||||
component.toggleSelected(tags[0])
|
||||
expect(component.selectedObjects.has(tags[0].id)).toBe(true)
|
||||
expect(component.togggleAll).toBe(false)
|
||||
expect(component.togggleAll()).toBe(false)
|
||||
|
||||
component.toggleSelected(tags[1])
|
||||
component.toggleSelected(tags[2])
|
||||
expect(component.togggleAll).toBe(true)
|
||||
expect(component.togggleAll()).toBe(true)
|
||||
|
||||
component.toggleSelected(tags[1])
|
||||
expect(component.selectedObjects.has(tags[1].id)).toBe(false)
|
||||
expect(component.togggleAll).toBe(false)
|
||||
expect(component.togggleAll()).toBe(false)
|
||||
})
|
||||
|
||||
it('areAllPageItemsSelected should return false when page has no selectable items', () => {
|
||||
component.data = []
|
||||
component.data.set([])
|
||||
component.selectedObjects.clear()
|
||||
|
||||
expect((component as any).areAllPageItemsSelected()).toBe(false)
|
||||
|
||||
component.data = tags
|
||||
component.data.set(tags)
|
||||
})
|
||||
|
||||
it('should support bulk edit permissions', () => {
|
||||
@@ -498,7 +497,7 @@ describe('ManagementListComponent', () => {
|
||||
document_count: 10,
|
||||
parent: 1,
|
||||
}
|
||||
component['unfilteredData'].push(childTag)
|
||||
component['unfilteredData'].update((data) => [...data, childTag])
|
||||
const original = component.getOriginalObject({ id: 4 } as Tag)
|
||||
expect(original).toEqual(childTag)
|
||||
})
|
||||
@@ -525,7 +524,7 @@ describe('ManagementListComponent', () => {
|
||||
expect(component.pageSize).toBe(25)
|
||||
})
|
||||
|
||||
it('pageSize setter should update settings, reset page and reload data on success', fakeAsync(() => {
|
||||
it('pageSize setter should update settings, reset page and reload data on success', () => {
|
||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
|
||||
@@ -536,21 +535,19 @@ describe('ManagementListComponent', () => {
|
||||
.mockReturnValue(of({ success: true }))
|
||||
|
||||
component.typeNamePlural = 'tags'
|
||||
component.page = 2
|
||||
component.page.set(2)
|
||||
component.pageSize = 100
|
||||
|
||||
tick()
|
||||
|
||||
expect(settingsService.set).toHaveBeenCalledWith(
|
||||
SETTINGS_KEYS.OBJECT_LIST_SIZES,
|
||||
{ tags: 100 }
|
||||
)
|
||||
expect(component.page).toBe(1)
|
||||
expect(component.page()).toBe(1)
|
||||
expect(reloadSpy).toHaveBeenCalled()
|
||||
expect(toastErrorSpy).not.toHaveBeenCalled()
|
||||
}))
|
||||
})
|
||||
|
||||
it('pageSize setter should show error toast on settings store failure', fakeAsync(() => {
|
||||
it('pageSize setter should show error toast on settings store failure', () => {
|
||||
const reloadSpy = jest.spyOn(component, 'reloadData')
|
||||
const toastErrorSpy = jest.spyOn(toastService, 'showError')
|
||||
|
||||
@@ -563,12 +560,10 @@ describe('ManagementListComponent', () => {
|
||||
component.typeNamePlural = 'tags'
|
||||
component.pageSize = 50
|
||||
|
||||
tick()
|
||||
|
||||
expect(toastErrorSpy).toHaveBeenCalledWith(
|
||||
'Error saving settings',
|
||||
expect.any(Error)
|
||||
)
|
||||
expect(reloadSpy).not.toHaveBeenCalled()
|
||||
}))
|
||||
})
|
||||
})
|
||||
|
||||
+42
-43
@@ -5,13 +5,13 @@ import {
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
QueryList,
|
||||
signal,
|
||||
ViewChildren,
|
||||
} from '@angular/core'
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { Subject } from 'rxjs'
|
||||
import {
|
||||
debounceTime,
|
||||
delay,
|
||||
distinctUntilChanged,
|
||||
takeUntil,
|
||||
tap,
|
||||
@@ -88,25 +88,25 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
|
||||
@ViewChildren(SortableDirective) headers: QueryList<SortableDirective>
|
||||
|
||||
public data: T[] = []
|
||||
private unfilteredData: T[] = []
|
||||
readonly data = signal<T[]>([])
|
||||
private readonly unfilteredData = signal<T[]>([])
|
||||
private currentExtraParams: { [key: string]: any } = null
|
||||
private allSelectionActive = false
|
||||
|
||||
public page = 1
|
||||
readonly page = signal(1)
|
||||
|
||||
public collectionSize = 0
|
||||
public displayCollectionSize = 0
|
||||
readonly collectionSize = signal(0)
|
||||
readonly displayCollectionSize = signal(0)
|
||||
|
||||
public sortField: string
|
||||
public sortReverse: boolean
|
||||
readonly sortField = signal<string>(undefined)
|
||||
readonly sortReverse = signal<boolean>(undefined)
|
||||
|
||||
private nameFilterDebounce: Subject<string>
|
||||
protected unsubscribeNotifier: Subject<any> = new Subject()
|
||||
protected _nameFilter: string
|
||||
|
||||
public selectedObjects: Set<number> = new Set()
|
||||
public togggleAll: boolean = false
|
||||
readonly togggleAll = signal(false)
|
||||
|
||||
public get hasSelection(): boolean {
|
||||
return this.selectedObjects.size > 0 || this.allSelectionActive
|
||||
@@ -114,7 +114,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
|
||||
public get selectedCount(): number {
|
||||
return this.allSelectionActive
|
||||
? this.displayCollectionSize
|
||||
? this.displayCollectionSize()
|
||||
: this.selectedObjects.size
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
)
|
||||
.subscribe((title) => {
|
||||
this._nameFilter = title
|
||||
this.page = 1
|
||||
this.page.set(1)
|
||||
this.reloadData()
|
||||
})
|
||||
}
|
||||
@@ -151,8 +151,8 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
}
|
||||
|
||||
onSort(event: SortEvent) {
|
||||
this.sortField = event.column
|
||||
this.sortReverse = event.reverse
|
||||
this.sortField.set(event.column)
|
||||
this.sortReverse.set(event.reverse)
|
||||
this.reloadData()
|
||||
}
|
||||
|
||||
@@ -171,25 +171,25 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
getDocumentCount(object: MatchingModel): number {
|
||||
return (
|
||||
object.document_count ??
|
||||
this.unfilteredData.find((d) => d.id == object.id)?.document_count ??
|
||||
this.unfilteredData().find((d) => d.id == object.id)?.document_count ??
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
public getOriginalObject(object: T): T {
|
||||
return this.unfilteredData.find((d) => d?.id == object?.id) || object
|
||||
return this.unfilteredData().find((d) => d?.id == object?.id) || object
|
||||
}
|
||||
|
||||
reloadData(extraParams: { [key: string]: any } = null) {
|
||||
this.loading = true
|
||||
this.loading.set(true)
|
||||
this.currentExtraParams = extraParams
|
||||
this.clearSelection()
|
||||
this.service
|
||||
.listFiltered(
|
||||
this.page,
|
||||
this.page(),
|
||||
this.pageSize,
|
||||
this.sortField,
|
||||
this.sortReverse,
|
||||
this.sortField(),
|
||||
this.sortReverse(),
|
||||
this._nameFilter,
|
||||
true,
|
||||
extraParams
|
||||
@@ -197,23 +197,22 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((c) => {
|
||||
this.unfilteredData = c.results
|
||||
this.data = this.filterData(c.results)
|
||||
this.collectionSize = this.getCollectionSize(c)
|
||||
this.displayCollectionSize = this.getDisplayCollectionSize(c)
|
||||
}),
|
||||
delay(100)
|
||||
this.unfilteredData.set(c.results)
|
||||
this.data.set(this.filterData(c.results))
|
||||
this.collectionSize.set(this.getCollectionSize(c))
|
||||
this.displayCollectionSize.set(this.getDisplayCollectionSize(c))
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
error: (error: HttpErrorResponse) => {
|
||||
if (error.error?.detail?.includes('Invalid page')) {
|
||||
this.page = 1
|
||||
this.page.set(1)
|
||||
this.reloadData()
|
||||
}
|
||||
},
|
||||
next: () => {
|
||||
this.show = true
|
||||
this.loading = false
|
||||
this.show.set(true)
|
||||
this.loading.set(false)
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -222,7 +221,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
const activeModal = this.modalService.open(this.editDialogComponent, {
|
||||
backdrop: 'static',
|
||||
})
|
||||
activeModal.componentInstance.dialogMode = EditDialogMode.CREATE
|
||||
activeModal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
|
||||
activeModal.componentInstance.succeeded.subscribe(() => {
|
||||
this.reloadData()
|
||||
this.toastService.showInfo(
|
||||
@@ -242,7 +241,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
backdrop: 'static',
|
||||
})
|
||||
activeModal.componentInstance.object = object
|
||||
activeModal.componentInstance.dialogMode = EditDialogMode.EDIT
|
||||
activeModal.componentInstance.dialogMode.set(EditDialogMode.EDIT)
|
||||
activeModal.componentInstance.succeeded.subscribe(() => {
|
||||
this.reloadData()
|
||||
this.toastService.showInfo(
|
||||
@@ -322,7 +321,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
})
|
||||
this.settingsService.storeSettings().subscribe({
|
||||
next: () => {
|
||||
this.page = 1
|
||||
this.page.set(1)
|
||||
this.reloadData()
|
||||
},
|
||||
error: (error) => {
|
||||
@@ -346,7 +345,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
if (!this.permissionsService.currentUserCan(action, this.permissionType))
|
||||
return false
|
||||
let ownsAll: boolean = true
|
||||
const objects = this.data.filter((o) => this.selectedObjects.has(o.id))
|
||||
const objects = this.data().filter((o) => this.selectedObjects.has(o.id))
|
||||
ownsAll = objects.every((o) =>
|
||||
this.permissionsService.currentUserOwnsObject(o)
|
||||
)
|
||||
@@ -367,7 +366,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
|
||||
clearSelection() {
|
||||
this.allSelectionActive = false
|
||||
this.togggleAll = false
|
||||
this.togggleAll.set(false)
|
||||
this.selectedObjects.clear()
|
||||
}
|
||||
|
||||
@@ -377,19 +376,19 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
|
||||
selectPage() {
|
||||
this.allSelectionActive = false
|
||||
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
||||
this.togggleAll = this.areAllPageItemsSelected()
|
||||
this.selectedObjects = new Set(this.getSelectableIDs(this.data()))
|
||||
this.togggleAll.set(this.areAllPageItemsSelected())
|
||||
}
|
||||
|
||||
selectAll() {
|
||||
if (!this.collectionSize) {
|
||||
if (!this.collectionSize()) {
|
||||
this.clearSelection()
|
||||
return
|
||||
}
|
||||
|
||||
this.allSelectionActive = true
|
||||
this.selectedObjects = new Set(this.getSelectableIDs(this.data))
|
||||
this.togggleAll = this.areAllPageItemsSelected()
|
||||
this.selectedObjects = new Set(this.getSelectableIDs(this.data()))
|
||||
this.togggleAll.set(this.areAllPageItemsSelected())
|
||||
}
|
||||
|
||||
toggleSelected(object) {
|
||||
@@ -399,14 +398,14 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
this.selectedObjects.has(object.id)
|
||||
? this.selectedObjects.delete(object.id)
|
||||
: this.selectedObjects.add(object.id)
|
||||
this.togggleAll = this.areAllPageItemsSelected()
|
||||
this.togggleAll.set(this.areAllPageItemsSelected())
|
||||
}
|
||||
|
||||
protected areAllPageItemsSelected(): boolean {
|
||||
if (this.allSelectionActive) {
|
||||
return this.data.length > 0
|
||||
return this.data().length > 0
|
||||
}
|
||||
const ids = this.getSelectableIDs(this.data)
|
||||
const ids = this.getSelectableIDs(this.data())
|
||||
return ids.length > 0 && ids.every((id) => this.selectedObjects.has(id))
|
||||
}
|
||||
|
||||
@@ -416,7 +415,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
})
|
||||
modal.componentInstance.confirmClicked.subscribe(
|
||||
({ permissions, merge }) => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.componentInstance.buttonsEnabled.set(false)
|
||||
this.service
|
||||
.bulk_edit_objects(
|
||||
this.allSelectionActive ? [] : Array.from(this.selectedObjects),
|
||||
@@ -435,7 +434,7 @@ export abstract class ManagementListComponent<T extends MatchingModel>
|
||||
this.reloadData()
|
||||
},
|
||||
error: (error) => {
|
||||
modal.componentInstance.buttonsEnabled = true
|
||||
modal.componentInstance.buttonsEnabled.set(true)
|
||||
this.toastService.showError(
|
||||
$localize`Error updating permissions`,
|
||||
error
|
||||
|
||||
+1
-1
@@ -136,7 +136,7 @@ describe('TagListComponent', () => {
|
||||
],
|
||||
}
|
||||
|
||||
component.data = [parent as any]
|
||||
component.data.set([parent as any])
|
||||
component.selectPage()
|
||||
|
||||
expect(component.selectedObjects.has(10)).toBe(true)
|
||||
|
||||
@@ -34,16 +34,16 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (loadingAccounts) {
|
||||
@if (loadingAccounts()) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (account of mailAccounts; track account) {
|
||||
@for (account of mailAccounts(); track account) {
|
||||
<li class="list-group-item">
|
||||
<div class="row fade" [class.show]="showAccounts">
|
||||
<div class="row fade" [class.show]="showAccounts()">
|
||||
<div class="col d-flex align-items-center">
|
||||
<button class="btn btn-link p-0 text-start" type="button" (click)="editMailAccount(account)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailAccount) || !userCanEdit(account)">
|
||||
{{account.name}}@switch (account.account_type) {
|
||||
@@ -91,7 +91,7 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (!loadingAccounts && mailAccounts.length === 0) {
|
||||
@if (!loadingAccounts() && mailAccounts().length === 0) {
|
||||
<li class="list-group-item" i18n>No mail accounts defined.</li>
|
||||
}
|
||||
</ul>
|
||||
@@ -117,19 +117,19 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (loadingRules) {
|
||||
@if (loadingRules()) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (rule of mailRules; track rule) {
|
||||
@for (rule of mailRules(); track rule) {
|
||||
<li class="list-group-item">
|
||||
<div class="row fade" [class.show]="showRules">
|
||||
<div class="row fade" [class.show]="showRules()">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editMailRule(rule)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.MailRule) || !userCanEdit(rule)">{{rule.name}}</button></div>
|
||||
<div class="col-1 d-flex align-items-center d-none d-sm-flex">{{rule.order}}</div>
|
||||
<div class="col-2 d-flex align-items-center">{{ mailAccountsById.get(rule.account)?.name }}</div>
|
||||
<div class="col-2 d-flex align-items-center">{{ mailAccountsById().get(rule.account)?.name }}</div>
|
||||
<div class="col-2 d-flex align-items-center d-none d-sm-flex">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input #inputField type="checkbox" class="form-check-input cursor-pointer" [id]="rule.id+'_enable'" [(ngModel)]="rule.enabled" (change)="onMailRuleEnableToggled(rule)" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.MailRule }">
|
||||
@@ -179,14 +179,14 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (!loadingRules && mailRules.length === 0) {
|
||||
@if (!loadingRules() && mailRules().length === 0) {
|
||||
<li class="list-group-item" i18n>No mail rules defined.</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
</ng-container>
|
||||
|
||||
@if (!mailAccounts || !mailRules) {
|
||||
@if (!mailAccounts() || !mailRules()) {
|
||||
<div>
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('MailComponent', () => {
|
||||
permissionsService = TestBed.inject(PermissionsService)
|
||||
activatedRoute = TestBed.inject(ActivatedRoute)
|
||||
settingsService = TestBed.inject(SettingsService)
|
||||
settingsService.currentUser = { id: 1 }
|
||||
settingsService.currentUser.set({ id: 1 })
|
||||
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
|
||||
jest
|
||||
.spyOn(permissionsService, 'currentUserHasObjectPermissions')
|
||||
@@ -261,7 +261,7 @@ describe('MailComponent', () => {
|
||||
const editDialog = modal.componentInstance as MailRuleEditDialogComponent
|
||||
expect(editDialog.object.id).toBeNull()
|
||||
expect(editDialog.object.name).toEqual(`${mailRules[0].name} (copy)`)
|
||||
expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE)
|
||||
expect(editDialog.dialogMode()).toEqual(EditDialogMode.CREATE)
|
||||
})
|
||||
|
||||
it('should support delete mail rule, show error if needed', () => {
|
||||
@@ -414,6 +414,6 @@ describe('MailComponent', () => {
|
||||
modalService.activeInstances.subscribe((refs) => (modal = refs[0]))
|
||||
component.viewProcessedMail(mailRules[0] as MailRule)
|
||||
const dialog = modal.componentInstance as any
|
||||
expect(dialog.rule).toEqual(mailRules[0])
|
||||
expect(dialog.rule()).toEqual(mailRules[0])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { Subject, delay, first, takeUntil, tap } from 'rxjs'
|
||||
import { Subject, first, takeUntil, tap } from 'rxjs'
|
||||
import { MailAccount, MailAccountType } from 'src/app/data/mail-account'
|
||||
import { MailRule } from 'src/app/data/mail-rule'
|
||||
import { ObjectWithPermissions } from 'src/app/data/object-with-permissions'
|
||||
@@ -56,19 +56,17 @@ export class MailComponent
|
||||
|
||||
public MailAccountType = MailAccountType
|
||||
|
||||
private _mailAccounts: MailAccount[] = []
|
||||
readonly mailAccounts = signal<MailAccount[]>([])
|
||||
|
||||
public get mailAccounts() {
|
||||
return this._mailAccounts
|
||||
}
|
||||
private set mailAccounts(accounts: MailAccount[]) {
|
||||
this._mailAccounts = accounts
|
||||
this.mailAccountsById = new Map(
|
||||
accounts.map((account) => [account.id, account])
|
||||
private setMailAccounts(accounts: MailAccount[]) {
|
||||
this.mailAccounts.set(accounts)
|
||||
this.mailAccountsById.set(
|
||||
new Map(accounts.map((account) => [account.id, account]))
|
||||
)
|
||||
}
|
||||
public mailAccountsById: Map<number, MailAccount> = new Map()
|
||||
public mailRules: MailRule[] = []
|
||||
readonly mailAccountsById = signal<Map<number, MailAccount>>(new Map())
|
||||
|
||||
readonly mailRules = signal<MailRule[]>([])
|
||||
|
||||
unsubscribeNotifier: Subject<any> = new Subject()
|
||||
oAuthAccountId: number
|
||||
@@ -81,10 +79,10 @@ export class MailComponent
|
||||
return this.settingsService.get(SETTINGS_KEYS.OUTLOOK_OAUTH_URL)
|
||||
}
|
||||
|
||||
public loadingRules: boolean = true
|
||||
public showRules: boolean = false
|
||||
public loadingAccounts: boolean = true
|
||||
public showAccounts: boolean = false
|
||||
readonly loadingRules = signal(true)
|
||||
readonly showRules = signal(false)
|
||||
readonly loadingAccounts = signal(true)
|
||||
readonly showAccounts = signal(false)
|
||||
|
||||
ngOnInit(): void {
|
||||
this.mailAccountService
|
||||
@@ -93,23 +91,21 @@ export class MailComponent
|
||||
first(),
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((r) => {
|
||||
this.mailAccounts = r.results
|
||||
this.setMailAccounts(r.results)
|
||||
this.loadingAccounts.set(false)
|
||||
this.showAccounts.set(true)
|
||||
if (this.oAuthAccountId) {
|
||||
this.editMailAccount(
|
||||
this.mailAccounts.find(
|
||||
this.mailAccounts().find(
|
||||
(account) => account.id === this.oAuthAccountId
|
||||
)
|
||||
)
|
||||
}
|
||||
}),
|
||||
delay(100)
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: () => {
|
||||
this.loadingAccounts = false
|
||||
this.showAccounts = true
|
||||
},
|
||||
error: (e) => {
|
||||
this.loadingAccounts.set(false)
|
||||
this.toastService.showError(
|
||||
$localize`Error retrieving mail accounts`,
|
||||
e
|
||||
@@ -123,16 +119,14 @@ export class MailComponent
|
||||
first(),
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((r) => {
|
||||
this.mailRules = r.results
|
||||
}),
|
||||
delay(100)
|
||||
this.mailRules.set(r.results)
|
||||
this.loadingRules.set(false)
|
||||
this.showRules.set(true)
|
||||
})
|
||||
)
|
||||
.subscribe({
|
||||
next: (r) => {
|
||||
this.loadingRules = false
|
||||
this.showRules = true
|
||||
},
|
||||
error: (e) => {
|
||||
this.loadingRules.set(false)
|
||||
this.toastService.showError($localize`Error retrieving mail rules`, e)
|
||||
},
|
||||
})
|
||||
@@ -143,9 +137,9 @@ export class MailComponent
|
||||
if (success) {
|
||||
this.toastService.showInfo($localize`OAuth2 authentication success`)
|
||||
this.oAuthAccountId = parseInt(params.get('account_id'))
|
||||
if (this.mailAccounts.length > 0) {
|
||||
if (this.mailAccounts().length > 0) {
|
||||
this.editMailAccount(
|
||||
this.mailAccounts.find(
|
||||
this.mailAccounts().find(
|
||||
(account) => account.id === this.oAuthAccountId
|
||||
)
|
||||
)
|
||||
@@ -168,9 +162,9 @@ export class MailComponent
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode = account
|
||||
? EditDialogMode.EDIT
|
||||
: EditDialogMode.CREATE
|
||||
modal.componentInstance.dialogMode.set(
|
||||
account ? EditDialogMode.EDIT : EditDialogMode.CREATE
|
||||
)
|
||||
modal.componentInstance.object = account
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
@@ -182,7 +176,7 @@ export class MailComponent
|
||||
this.mailAccountService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
this.mailAccounts = r.results
|
||||
this.setMailAccounts(r.results)
|
||||
})
|
||||
})
|
||||
modal.componentInstance.failed
|
||||
@@ -213,7 +207,7 @@ export class MailComponent
|
||||
this.mailAccountService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
this.mailAccounts = r.results
|
||||
this.setMailAccounts(r.results)
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
@@ -247,8 +241,9 @@ export class MailComponent
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode =
|
||||
modal.componentInstance.dialogMode.set(
|
||||
rule && !forceCreate ? EditDialogMode.EDIT : EditDialogMode.CREATE
|
||||
)
|
||||
modal.componentInstance.object = rule
|
||||
modal.componentInstance.succeeded
|
||||
.pipe(takeUntil(this.unsubscribeNotifier))
|
||||
@@ -258,7 +253,7 @@ export class MailComponent
|
||||
this.mailRuleService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
this.mailRules = r.results
|
||||
this.mailRules.set(r.results)
|
||||
})
|
||||
})
|
||||
modal.componentInstance.failed
|
||||
@@ -314,7 +309,7 @@ export class MailComponent
|
||||
this.mailRuleService
|
||||
.listAll(null, null, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
this.mailRules = r.results
|
||||
this.mailRules.set(r.results)
|
||||
})
|
||||
},
|
||||
error: (e) => {
|
||||
@@ -336,7 +331,7 @@ export class MailComponent
|
||||
dialog.object = object
|
||||
modal.componentInstance.confirmClicked.subscribe(
|
||||
({ permissions, merge }) => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.componentInstance.buttonsEnabled.set(false)
|
||||
const service: AbstractPaperlessService<MailRule | MailAccount> =
|
||||
'account' in object ? this.mailRuleService : this.mailAccountService
|
||||
object.owner = permissions['owner']
|
||||
@@ -362,7 +357,7 @@ export class MailComponent
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.rule = rule
|
||||
modal.componentInstance.rule.set(rule)
|
||||
}
|
||||
|
||||
userCanEdit(obj: ObjectWithPermissions): boolean {
|
||||
|
||||
+9
-9
@@ -1,5 +1,5 @@
|
||||
<div class="modal-header">
|
||||
<h6 class="modal-title" id="modal-basic-title" i18n>Processed Mail for <em>{{ rule.name }}</em></h6>
|
||||
<h6 class="modal-title" id="modal-basic-title" i18n>Processed Mail for <em>{{ rule().name }}</em></h6>
|
||||
<button class="btn btn-sm btn-link text-muted me-auto p-0 p-md-2" title="What's this?" i18n-title type="button" [ngbPopover]="infoPopover" [autoClose]="true">
|
||||
<i-bs name="question-circle"></i-bs>
|
||||
</button>
|
||||
@@ -10,13 +10,13 @@
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="close()"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@if (loading) {
|
||||
@if (loading()) {
|
||||
<div class="text-center my-5">
|
||||
<div class="spinner-border" role="status">
|
||||
<span class="visually-hidden" i18n>Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (processedMails.length === 0) {
|
||||
} @else if (processedMails().length === 0) {
|
||||
<span i18n>No processed email messages found.</span>
|
||||
} @else {
|
||||
<div class="table-responsive">
|
||||
@@ -25,7 +25,7 @@
|
||||
<tr>
|
||||
<th scope="col" style="width: 40px;">
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [(ngModel)]="toggleAllEnabled" [disabled]="processedMails.length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input" id="all-objects" [ngModel]="toggleAllEnabled()" (ngModelChange)="toggleAllEnabled.set($event)" [disabled]="processedMails().length === 0" (click)="toggleAll($event); $event.stopPropagation();">
|
||||
<label class="form-check-label" for="all-objects"></label>
|
||||
</div>
|
||||
</th>
|
||||
@@ -37,7 +37,7 @@
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (mail of processedMails; track mail.id) {
|
||||
@for (mail of processedMails(); track mail.id) {
|
||||
<ng-template #statusTooltip>
|
||||
<div class="small text-light font-monospace">
|
||||
{{mail.status}}
|
||||
@@ -46,7 +46,7 @@
|
||||
<tr>
|
||||
<td>
|
||||
<div class="form-check m-0 ms-2 me-n2">
|
||||
<input type="checkbox" class="form-check-input" [id]="mail.id" [checked]="selectedMailIds.has(mail.id)" (click)="toggleSelected(mail); $event.stopPropagation();">
|
||||
<input type="checkbox" class="form-check-input" [id]="mail.id" [checked]="selectedMailIds().has(mail.id)" (click)="toggleSelected(mail); $event.stopPropagation();">
|
||||
<label class="form-check-label" [for]="mail.id"></label>
|
||||
</div>
|
||||
</td>
|
||||
@@ -82,7 +82,7 @@
|
||||
</table>
|
||||
</div>
|
||||
<div class="btn-toolbar">
|
||||
<button type="button" class="btn btn-outline-secondary me-2" (click)="clearSelection()" [disabled]="selectedMailIds.size === 0" i18n>Clear</button>
|
||||
<button type="button" class="btn btn-outline-secondary me-2" (click)="clearSelection()" [disabled]="selectedMailIds().size === 0" i18n>Clear</button>
|
||||
<pngx-confirm-button
|
||||
label="Delete selected"
|
||||
i18n-label
|
||||
@@ -90,12 +90,12 @@
|
||||
i18n-title
|
||||
buttonClasses="btn-outline-danger"
|
||||
iconName="trash"
|
||||
[disabled]="selectedMailIds.size === 0"
|
||||
[disabled]="selectedMailIds().size === 0"
|
||||
(confirm)="deleteSelected()">
|
||||
</pngx-confirm-button>
|
||||
<div class="ms-auto">
|
||||
<ngb-pagination
|
||||
[collectionSize]="processedMails.length"
|
||||
[collectionSize]="processedMails().length"
|
||||
[(page)]="page"
|
||||
[pageSize]="50"
|
||||
[maxSize]="5"
|
||||
|
||||
+10
-10
@@ -64,7 +64,7 @@ describe('ProcessedMailDialogComponent', () => {
|
||||
toastService = TestBed.inject(ToastService)
|
||||
fixture = TestBed.createComponent(ProcessedMailDialogComponent)
|
||||
component = fixture.componentInstance
|
||||
component.rule = rule
|
||||
component.rule.set(rule)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -83,8 +83,8 @@ describe('ProcessedMailDialogComponent', () => {
|
||||
fixture.detectChanges()
|
||||
const req = expectListRequest(rule.id)
|
||||
req.flush({ count: 2, results: mails })
|
||||
expect(component.loading).toBeFalsy()
|
||||
expect(component.processedMails).toEqual(mails)
|
||||
expect(component.loading()).toBeFalsy()
|
||||
expect(component.processedMails()).toEqual(mails)
|
||||
})
|
||||
|
||||
it('should delete selected mails and reload', () => {
|
||||
@@ -94,8 +94,8 @@ describe('ProcessedMailDialogComponent', () => {
|
||||
initialReq.flush({ count: 0, results: [] })
|
||||
|
||||
// select a couple of mails and delete
|
||||
component.selectedMailIds.add(5)
|
||||
component.selectedMailIds.add(6)
|
||||
component.selectedMailIds().add(5)
|
||||
component.selectedMailIds().add(6)
|
||||
const toastInfoSpy = jest.spyOn(toastService, 'showInfo')
|
||||
component.deleteSelected()
|
||||
|
||||
@@ -127,18 +127,18 @@ describe('ProcessedMailDialogComponent', () => {
|
||||
header.dispatchEvent(new Event('click'))
|
||||
header.checked = true
|
||||
header.dispatchEvent(new Event('click'))
|
||||
expect(component.selectedMailIds.size).toEqual(mails.length)
|
||||
expect(component.selectedMailIds().size).toEqual(mails.length)
|
||||
|
||||
// toggle a single mail
|
||||
component.toggleSelected(mails[0] as any)
|
||||
expect(component.selectedMailIds.has(mails[0].id)).toBeFalsy()
|
||||
expect(component.selectedMailIds().has(mails[0].id)).toBeFalsy()
|
||||
component.toggleSelected(mails[0] as any)
|
||||
expect(component.selectedMailIds.has(mails[0].id)).toBeTruthy()
|
||||
expect(component.selectedMailIds().has(mails[0].id)).toBeTruthy()
|
||||
|
||||
// clear selection
|
||||
component.clearSelection()
|
||||
expect(component.selectedMailIds.size).toEqual(0)
|
||||
expect(component.toggleAllEnabled).toBeFalsy()
|
||||
expect(component.selectedMailIds().size).toEqual(0)
|
||||
expect(component.toggleAllEnabled()).toBeFalsy()
|
||||
})
|
||||
|
||||
it('should close the dialog', () => {
|
||||
|
||||
+21
-20
@@ -1,5 +1,5 @@
|
||||
import { SlicePipe } from '@angular/common'
|
||||
import { Component, inject, Input, OnInit } from '@angular/core'
|
||||
import { Component, inject, OnInit, signal } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import {
|
||||
NgbActiveModal,
|
||||
@@ -36,16 +36,14 @@ export class ProcessedMailDialogComponent implements OnInit {
|
||||
private readonly processedMailService = inject(ProcessedMailService)
|
||||
private readonly toastService = inject(ToastService)
|
||||
|
||||
public processedMails: ProcessedMail[] = []
|
||||
|
||||
public loading: boolean = true
|
||||
public toggleAllEnabled: boolean = false
|
||||
public readonly selectedMailIds: Set<number> = new Set<number>()
|
||||
readonly rule = signal<MailRule>(undefined)
|
||||
readonly processedMails = signal<ProcessedMail[]>([])
|
||||
readonly loading = signal(true)
|
||||
readonly toggleAllEnabled = signal(false)
|
||||
readonly selectedMailIds = signal<Set<number>>(new Set())
|
||||
|
||||
public page: number = 1
|
||||
|
||||
@Input() rule: MailRule
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadProcessedMails()
|
||||
}
|
||||
@@ -55,19 +53,19 @@ export class ProcessedMailDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
private loadProcessedMails(): void {
|
||||
this.loading = true
|
||||
this.loading.set(true)
|
||||
this.clearSelection()
|
||||
this.processedMailService
|
||||
.list(this.page, 50, 'processed_at', true, { rule: this.rule.id })
|
||||
.list(this.page, 50, 'processed_at', true, { rule: this.rule().id })
|
||||
.subscribe((result) => {
|
||||
this.processedMails = result.results
|
||||
this.loading = false
|
||||
this.processedMails.set(result.results)
|
||||
this.loading.set(false)
|
||||
})
|
||||
}
|
||||
|
||||
public deleteSelected(): void {
|
||||
this.processedMailService
|
||||
.bulk_delete(Array.from(this.selectedMailIds))
|
||||
.bulk_delete(Array.from(this.selectedMailIds()))
|
||||
.subscribe(() => {
|
||||
this.toastService.showInfo($localize`Processed mail(s) deleted`)
|
||||
this.loadProcessedMails()
|
||||
@@ -75,22 +73,25 @@ export class ProcessedMailDialogComponent implements OnInit {
|
||||
}
|
||||
|
||||
public toggleAll(event: PointerEvent) {
|
||||
const selectedMailIds = new Set<number>()
|
||||
if ((event.target as HTMLInputElement).checked) {
|
||||
this.selectedMailIds.clear()
|
||||
this.processedMails.forEach((mail) => this.selectedMailIds.add(mail.id))
|
||||
this.processedMails().forEach((mail) => selectedMailIds.add(mail.id))
|
||||
this.selectedMailIds.set(selectedMailIds)
|
||||
} else {
|
||||
this.clearSelection()
|
||||
}
|
||||
}
|
||||
|
||||
public clearSelection() {
|
||||
this.toggleAllEnabled = false
|
||||
this.selectedMailIds.clear()
|
||||
this.toggleAllEnabled.set(false)
|
||||
this.selectedMailIds.set(new Set())
|
||||
}
|
||||
|
||||
public toggleSelected(mail: ProcessedMail) {
|
||||
this.selectedMailIds.has(mail.id)
|
||||
? this.selectedMailIds.delete(mail.id)
|
||||
: this.selectedMailIds.add(mail.id)
|
||||
const selectedMailIds = new Set(this.selectedMailIds())
|
||||
selectedMailIds.has(mail.id)
|
||||
? selectedMailIds.delete(mail.id)
|
||||
: selectedMailIds.add(mail.id)
|
||||
this.selectedMailIds.set(selectedMailIds)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</pngx-page-header>
|
||||
<form [formGroup]="savedViewsForm" (ngSubmit)="save()">
|
||||
<ul class="list-group mb-3" formGroupName="savedViews">
|
||||
@for (view of savedViews; track view) {
|
||||
@for (view of savedViews(); track view) {
|
||||
<li class="list-group-item py-3">
|
||||
<div [formGroupName]="view.id">
|
||||
<div class="row">
|
||||
@@ -65,13 +65,13 @@
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (savedViews && savedViews.length === 0) {
|
||||
@if (savedViews() && savedViews().length === 0) {
|
||||
<li class="list-group-item">
|
||||
<div i18n>No saved views defined.</div>
|
||||
</li>
|
||||
}
|
||||
|
||||
@if (loading) {
|
||||
@if (loading()) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm fw-normal ms-2 me-auto" role="status"></div>
|
||||
<div class="visually-hidden" i18n>Loading...</div>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop'
|
||||
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing'
|
||||
import { signal } from '@angular/core'
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
|
||||
@@ -226,7 +227,8 @@ describe('SavedViewsComponent', () => {
|
||||
const modalRef = {
|
||||
componentInstance: {
|
||||
confirmClicked,
|
||||
buttonsEnabled: true,
|
||||
buttonsEnabled: signal(true),
|
||||
note: signal(null),
|
||||
},
|
||||
close: jest.fn(),
|
||||
} as any
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AsyncPipe } from '@angular/common'
|
||||
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
|
||||
import { Component, OnDestroy, OnInit, inject, signal } from '@angular/core'
|
||||
import {
|
||||
FormControl,
|
||||
FormGroup,
|
||||
@@ -56,7 +56,7 @@ export class SavedViewsComponent
|
||||
|
||||
DisplayMode = DisplayMode
|
||||
|
||||
public savedViews: SavedView[]
|
||||
readonly savedViews = signal<SavedView[]>(undefined)
|
||||
private savedViewsGroup = new FormGroup({})
|
||||
public savedViewsForm: FormGroup = new FormGroup({
|
||||
savedViews: this.savedViewsGroup,
|
||||
@@ -66,12 +66,12 @@ export class SavedViewsComponent
|
||||
public isDirty$: Observable<boolean>
|
||||
|
||||
get displayFields() {
|
||||
return this.settings.allDisplayFields
|
||||
return this.settings.allDisplayFields()
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.settings.organizingSidebarSavedViews = true
|
||||
this.settings.organizingSidebarSavedViews.set(true)
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
@@ -79,29 +79,29 @@ export class SavedViewsComponent
|
||||
}
|
||||
|
||||
private reloadViews(): void {
|
||||
this.loading = true
|
||||
this.loading.set(true)
|
||||
this.savedViewService
|
||||
.list(null, null, null, false, { full_perms: true })
|
||||
.subscribe((r) => {
|
||||
this.savedViews = r.results
|
||||
this.savedViews.set(r.results)
|
||||
this.initialize()
|
||||
})
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.settings.organizingSidebarSavedViews = false
|
||||
this.settings.organizingSidebarSavedViews.set(false)
|
||||
super.ngOnDestroy()
|
||||
}
|
||||
|
||||
private initialize() {
|
||||
this.loading = false
|
||||
this.loading.set(false)
|
||||
this.emptyGroup(this.savedViewsGroup)
|
||||
|
||||
let storeData = {
|
||||
savedViews: {},
|
||||
}
|
||||
|
||||
for (let view of this.savedViews) {
|
||||
for (let view of this.savedViews()) {
|
||||
storeData.savedViews[view.id.toString()] = {
|
||||
id: view.id,
|
||||
name: view.name,
|
||||
@@ -148,7 +148,9 @@ export class SavedViewsComponent
|
||||
public deleteSavedView(savedView: SavedView) {
|
||||
this.savedViewService.delete(savedView).subscribe(() => {
|
||||
this.savedViewsGroup.removeControl(savedView.id.toString())
|
||||
this.savedViews.splice(this.savedViews.indexOf(savedView), 1)
|
||||
this.savedViews.update((savedViews) =>
|
||||
savedViews.filter((view) => view !== savedView)
|
||||
)
|
||||
this.toastService.showInfo(
|
||||
$localize`Saved view "${savedView.name}" deleted.`
|
||||
)
|
||||
@@ -254,10 +256,12 @@ export class SavedViewsComponent
|
||||
})
|
||||
const dialog = modal.componentInstance as PermissionsDialogComponent
|
||||
dialog.object = savedView
|
||||
dialog.note = $localize`Note: Sharing saved views does not share the underlying documents.`
|
||||
dialog.note.set(
|
||||
$localize`Note: Sharing saved views does not share the underlying documents.`
|
||||
)
|
||||
|
||||
modal.componentInstance.confirmClicked.subscribe(({ permissions }) => {
|
||||
modal.componentInstance.buttonsEnabled = false
|
||||
modal.componentInstance.buttonsEnabled.set(false)
|
||||
const view = {
|
||||
id: savedView.id,
|
||||
owner: permissions.owner,
|
||||
|
||||
@@ -22,16 +22,16 @@
|
||||
</div>
|
||||
</li>
|
||||
|
||||
@if (loading && workflows.length === 0) {
|
||||
@if (loading() && workflows().length === 0) {
|
||||
<li class="list-group-item">
|
||||
<div class="spinner-border spinner-border-sm me-2" role="status"></div>
|
||||
<ng-container i18n>Loading...</ng-container>
|
||||
</li>
|
||||
}
|
||||
|
||||
@for (workflow of workflows; track workflow.id) {
|
||||
@for (workflow of workflows(); track workflow.id) {
|
||||
<li class="list-group-item">
|
||||
<div class="row fade" [class.show]="show">
|
||||
<div class="row fade" [class.show]="show()">
|
||||
<div class="col d-flex align-items-center"><button class="btn btn-link p-0 text-start" type="button" (click)="editWorkflow(workflow)" [disabled]="!permissionsService.currentUserCan(PermissionAction.Change, PermissionType.Workflow)">{{workflow.name}}</button></div>
|
||||
<div class="col d-flex align-items-center d-none d-sm-flex"><code>{{workflow.order}}</code></div>
|
||||
<div class="col d-flex align-items-center">
|
||||
@@ -76,7 +76,7 @@
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
@if (!loading && workflows.length === 0) {
|
||||
<li class="list-group-item" [class.show]="show" i18n>No workflows defined.</li>
|
||||
@if (!loading() && workflows().length === 0) {
|
||||
<li class="list-group-item" [class.show]="show()" i18n>No workflows defined.</li>
|
||||
}
|
||||
</ul>
|
||||
|
||||
@@ -183,7 +183,7 @@ describe('WorkflowsComponent', () => {
|
||||
expect(modal).not.toBeUndefined()
|
||||
const editDialog = modal.componentInstance as WorkflowEditDialogComponent
|
||||
expect(editDialog.object.name).toEqual(workflows[0].name + ' (copy)')
|
||||
expect(editDialog.dialogMode).toEqual(EditDialogMode.CREATE)
|
||||
expect(editDialog.dialogMode()).toEqual(EditDialogMode.CREATE)
|
||||
})
|
||||
|
||||
it('should null ids on copy', () => {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { Component, OnInit, inject } from '@angular/core'
|
||||
import { Component, OnInit, inject, signal } from '@angular/core'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { NgbDropdownModule, NgbModal } from '@ng-bootstrap/ng-bootstrap'
|
||||
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
|
||||
import { delay, takeUntil, tap } from 'rxjs'
|
||||
import { takeUntil, tap } from 'rxjs'
|
||||
import { Workflow } from 'src/app/data/workflow'
|
||||
import { IfPermissionsDirective } from 'src/app/directives/if-permissions.directive'
|
||||
import { PermissionsService } from 'src/app/services/permissions.service'
|
||||
@@ -39,24 +39,23 @@ export class WorkflowsComponent
|
||||
private modalService = inject(NgbModal)
|
||||
private toastService = inject(ToastService)
|
||||
|
||||
public workflows: Workflow[] = []
|
||||
readonly workflows = signal<Workflow[]>([])
|
||||
|
||||
ngOnInit() {
|
||||
this.reload()
|
||||
}
|
||||
|
||||
reload() {
|
||||
this.loading = true
|
||||
this.loading.set(true)
|
||||
this.workflowService
|
||||
.listAll()
|
||||
.pipe(
|
||||
takeUntil(this.unsubscribeNotifier),
|
||||
tap((r) => (this.workflows = r.results)),
|
||||
delay(100)
|
||||
tap((r) => this.workflows.set(r.results))
|
||||
)
|
||||
.subscribe(() => {
|
||||
this.show = true
|
||||
this.loading = false
|
||||
this.show.set(true)
|
||||
this.loading.set(false)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -74,8 +73,9 @@ export class WorkflowsComponent
|
||||
backdrop: 'static',
|
||||
size: 'xl',
|
||||
})
|
||||
modal.componentInstance.dialogMode =
|
||||
modal.componentInstance.dialogMode.set(
|
||||
workflow && !forceCreate ? EditDialogMode.EDIT : EditDialogMode.CREATE
|
||||
)
|
||||
if (workflow) {
|
||||
// quick "deep" clone so original doesn't get modified
|
||||
const clone = Object.assign({}, workflow)
|
||||
|
||||
Reference in New Issue
Block a user