Again, removing wrappers

This commit is contained in:
shamoon
2026-07-09 00:20:02 -07:00
parent b3f0985ed4
commit d3d871e0cf
39 changed files with 263 additions and 510 deletions
@@ -9,9 +9,9 @@
<label class="form-label" for="metadataDocumentID" i18n>Documents:</label>
<ul class="list-group"
cdkDropList
[cdkDropListData]="documentIDs"
[cdkDropListData]="documentIDs()"
(cdkDropListDropped)="onDrop($event)">
@for (documentID of documentIDs; track documentID) {
@for (documentID of documentIDs(); track documentID) {
@let document = getDocument(documentID);
@if (document) {
<li class="list-group-item d-flex align-items-center" cdkDrag>
@@ -36,22 +36,22 @@
</div>
<div class="form-group mt-4">
<label class="form-label" for="metadataDocumentID" i18n>Use metadata from:</label>
<select class="form-select" [(ngModel)]="metadataDocumentID">
<select class="form-select" [ngModel]="metadataDocumentID()" (ngModelChange)="metadataDocumentID.set($event)">
<option [ngValue]="-1" i18n>Regenerate all metadata</option>
@for (document of documents; track document.id) {
@for (document of documents(); track document.id) {
<option [ngValue]="document.id">{{document.title}}</option>
}
</select>
</div>
<div class="form-check form-switch mt-4">
<input class="form-check-input" type="checkbox" role="switch" id="archiveFallbackSwitch" [(ngModel)]="archiveFallback">
<input class="form-check-input" type="checkbox" role="switch" id="archiveFallbackSwitch" [ngModel]="archiveFallback()" (ngModelChange)="archiveFallback.set($event)">
<label class="form-check-label" for="archiveFallbackSwitch" i18n>Try to include archive version in merge for non-PDF files</label>
</div>
<div class="form-check form-switch mt-2">
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalsSwitch" [(ngModel)]="deleteOriginals" [disabled]="!userOwnsAllDocuments">
<input class="form-check-input" type="checkbox" role="switch" id="deleteOriginalsSwitch" [ngModel]="deleteOriginals()" (ngModelChange)="deleteOriginals.set($event)" [disabled]="!userOwnsAllDocuments">
<label class="form-check-label" for="deleteOriginalsSwitch" i18n>Delete original documents after successful merge</label>
</div>
@if (!archiveFallback) {
@if (!archiveFallback()) {
<p class="small text-muted fst-italic mt-4" i18n>Note that only PDFs will be included.</p>
}
</div>
@@ -50,12 +50,12 @@ describe('MergeConfirmDialogComponent', () => {
component.ngOnInit()
expect(component.documents).toEqual(documents)
expect(documentService.getFew).toHaveBeenCalledWith(component.documentIDs)
expect(component.documents()).toEqual(documents)
expect(documentService.getFew).toHaveBeenCalledWith(component.documentIDs())
})
it('should move documentIDs on drop', () => {
component.documentIDs = [1, 2, 3]
component.documentIDs.set([1, 2, 3])
const event = {
previousIndex: 1,
currentIndex: 2,
@@ -63,7 +63,7 @@ describe('MergeConfirmDialogComponent', () => {
component.onDrop(event as any)
expect(component.documentIDs).toEqual([1, 3, 2])
expect(component.documentIDs()).toEqual([1, 3, 2])
})
it('should get document by ID', () => {
@@ -36,47 +36,11 @@ export class MergeConfirmDialogComponent
private documentService = inject(DocumentService)
private permissionService = inject(PermissionsService)
private documentIDsSignal = signal<number[]>([])
private archiveFallbackSignal = signal(false)
private deleteOriginalsSignal = signal(false)
private documentsSignal = signal<Document[]>([])
private metadataDocumentIDSignal = signal(-1)
public get documentIDs(): number[] {
return this.documentIDsSignal()
}
public set documentIDs(documentIDs: number[]) {
this.documentIDsSignal.set(documentIDs)
}
public get archiveFallback(): boolean {
return this.archiveFallbackSignal()
}
public set archiveFallback(archiveFallback: boolean) {
this.archiveFallbackSignal.set(archiveFallback)
}
public get deleteOriginals(): boolean {
return this.deleteOriginalsSignal()
}
public set deleteOriginals(deleteOriginals: boolean) {
this.deleteOriginalsSignal.set(deleteOriginals)
}
get documents(): Document[] {
return this.documentsSignal()
}
public get metadataDocumentID(): number {
return this.metadataDocumentIDSignal()
}
public set metadataDocumentID(metadataDocumentID: number) {
this.metadataDocumentIDSignal.set(metadataDocumentID)
}
readonly documentIDs = signal<number[]>([])
readonly archiveFallback = signal(false)
readonly deleteOriginals = signal(false)
readonly documents = signal<Document[]>([])
readonly metadataDocumentID = signal(-1)
constructor() {
super()
@@ -84,25 +48,25 @@ export class MergeConfirmDialogComponent
ngOnInit() {
this.documentService
.getFew(this.documentIDs)
.getFew(this.documentIDs())
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe((r) => {
this.documentsSignal.set(r.results)
this.documents.set(r.results)
})
}
onDrop(event: CdkDragDrop<number[]>) {
const documentIDs = this.documentIDs.concat()
const documentIDs = this.documentIDs().concat()
moveItemInArray(documentIDs, event.previousIndex, event.currentIndex)
this.documentIDs = documentIDs
this.documentIDs.set(documentIDs)
}
getDocument(documentID: number): Document {
return this.documents.find((d) => d.id === documentID)
return this.documents().find((d) => d.id === documentID)
}
get userOwnsAllDocuments(): boolean {
return this.documents.every((d) =>
return this.documents().every((d) =>
this.permissionService.currentUserOwnsObject(d)
)
}
@@ -13,7 +13,7 @@
<pngx-input-text [horizontal]="true" i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
</div>
<div class="col-md-4">
<pngx-input-select [horizontal]="true" i18n-title title="Account" [items]="accounts" formControlName="account"></pngx-input-select>
<pngx-input-select [horizontal]="true" i18n-title title="Account" [items]="accounts()" formControlName="account"></pngx-input-select>
</div>
<div class="col-md-2 pt-2">
<pngx-input-switch [horizontal]="true" i18n-title title="Enabled" formControlName="enabled"></pngx-input-switch>
@@ -65,10 +65,10 @@
</div>
<div class="col-md-6">
<pngx-input-tags [horizontal]="true" [allowCreate]="false" formControlName="assign_tags"></pngx-input-tags>
<pngx-input-select [horizontal]="true" i18n-title title="Assign document type" [items]="documentTypes" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
<pngx-input-select [horizontal]="true" i18n-title title="Assign document type" [items]="documentTypes()" [allowNull]="true" formControlName="assign_document_type"></pngx-input-select>
<pngx-input-select [horizontal]="true" i18n-title title="Assign correspondent from" [items]="metadataCorrespondentOptions" formControlName="assign_correspondent_from"></pngx-input-select>
@if (showCorrespondentField) {
<pngx-input-select [horizontal]="true" i18n-title title="Assign correspondent" [items]="correspondents" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
<pngx-input-select [horizontal]="true" i18n-title title="Assign correspondent" [items]="correspondents()" [allowNull]="true" formControlName="assign_correspondent"></pngx-input-select>
}
</div>
</div>
@@ -1,11 +1,12 @@
import { Component, inject, signal } from '@angular/core'
import { Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import {
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
} from '@angular/forms'
import { first } from 'rxjs'
import { map } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { Correspondent } from 'src/app/data/correspondent'
import { DocumentType } from 'src/app/data/document-type'
@@ -154,61 +155,28 @@ const METADATA_CORRESPONDENT_OPTIONS = [
],
})
export class MailRuleEditDialogComponent extends EditDialogComponent<MailRule> {
private accountService: MailAccountService
private correspondentService: CorrespondentService
private documentTypeService: DocumentTypeService
private accountService = inject(MailAccountService)
private correspondentService = inject(CorrespondentService)
private documentTypeService = inject(DocumentTypeService)
private accountsSignal = signal<MailAccount[]>(undefined)
private correspondentsSignal = signal<Correspondent[]>(undefined)
private documentTypesSignal = signal<DocumentType[]>(undefined)
get accounts(): MailAccount[] {
return this.accountsSignal()
}
set accounts(accounts: MailAccount[]) {
this.accountsSignal.set(accounts)
}
get correspondents(): Correspondent[] {
return this.correspondentsSignal()
}
set correspondents(correspondents: Correspondent[]) {
this.correspondentsSignal.set(correspondents)
}
get documentTypes(): DocumentType[] {
return this.documentTypesSignal()
}
set documentTypes(documentTypes: DocumentType[]) {
this.documentTypesSignal.set(documentTypes)
}
readonly accounts = toSignal(
this.accountService.listAll().pipe(map((result) => result.results)),
{ initialValue: undefined as MailAccount[] }
)
readonly correspondents = toSignal(
this.correspondentService.listAll().pipe(map((result) => result.results)),
{ initialValue: undefined as Correspondent[] }
)
readonly documentTypes = toSignal(
this.documentTypeService.listAll().pipe(map((result) => result.results)),
{ initialValue: undefined as DocumentType[] }
)
constructor() {
super()
this.service = inject(MailRuleService)
this.accountService = inject(MailAccountService)
this.correspondentService = inject(CorrespondentService)
this.documentTypeService = inject(DocumentTypeService)
this.userService = inject(UserService)
this.settingsService = inject(SettingsService)
this.accountService
.listAll()
.pipe(first())
.subscribe((result) => (this.accounts = result.results))
this.correspondentService
.listAll()
.pipe(first())
.subscribe((result) => (this.correspondents = result.results))
this.documentTypeService
.listAll()
.pipe(first())
.subscribe((result) => (this.documentTypes = result.results))
}
getCreateTitle() {
@@ -31,7 +31,7 @@
</div>
</div>
<pngx-input-select i18n-title title="Groups" [items]="groups" multiple="true" formControlName="groups"></pngx-input-select>
<pngx-input-select i18n-title title="Groups" [items]="groups()" multiple="true" formControlName="groups"></pngx-input-select>
@if (object?.is_mfa_enabled && currentUserIsSuperUser) {
<label class="form-label" i18n>Two-factor Authentication</label>
@@ -42,7 +42,7 @@
i18n-title
buttonClasses="btn-outline-danger btn-sm"
iconName="trash"
[disabled]="totpLoading"
[disabled]="totpLoading()"
(confirm)="deactivateTotp()">
</pngx-confirm-button>
}
@@ -114,17 +114,17 @@ describe('UserEditDialogComponent', () => {
it('should detect whether password was changed in form on save', () => {
component.objectForm.get('password').setValue(null)
component.save()
expect(component.passwordIsSet).toBeFalsy()
expect(component.passwordIsSet()).toBeFalsy()
// unchanged pw
component.objectForm.get('password').setValue('*******')
component.save()
expect(component.passwordIsSet).toBeFalsy()
expect(component.passwordIsSet()).toBeFalsy()
// unchanged pw
component.objectForm.get('password').setValue('helloworld')
component.save()
expect(component.passwordIsSet).toBeTruthy()
expect(component.passwordIsSet()).toBeTruthy()
})
it('should support deactivation of TOTP', () => {
@@ -1,11 +1,12 @@
import { Component, OnInit, inject, signal } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import {
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
} from '@angular/forms'
import { first } from 'rxjs'
import { first, map } from 'rxjs'
import { EditDialogComponent } from 'src/app/components/common/edit-dialog/edit-dialog.component'
import { Group } from 'src/app/data/group'
import { User } from 'src/app/data/user'
@@ -38,46 +39,19 @@ export class UserEditDialogComponent
implements OnInit
{
private toastService = inject(ToastService)
private groupsService: GroupService
private groupsService = inject(GroupService)
private groupsSignal = signal<Group[]>(undefined)
private passwordIsSetSignal = signal(false)
private totpLoadingSignal = signal(false)
get groups(): Group[] {
return this.groupsSignal()
}
set groups(groups: Group[]) {
this.groupsSignal.set(groups)
}
get passwordIsSet(): boolean {
return this.passwordIsSetSignal()
}
set passwordIsSet(passwordIsSet: boolean) {
this.passwordIsSetSignal.set(passwordIsSet)
}
public get totpLoading(): boolean {
return this.totpLoadingSignal()
}
public set totpLoading(totpLoading: boolean) {
this.totpLoadingSignal.set(totpLoading)
}
readonly groups = toSignal(
this.groupsService.listAll().pipe(map((result) => result.results)),
{ initialValue: undefined as Group[] }
)
readonly passwordIsSet = signal(false)
readonly totpLoading = signal(false)
constructor() {
super()
this.service = inject(UserService)
this.groupsService = inject(GroupService)
this.settingsService = inject(SettingsService)
this.groupsService
.listAll()
.pipe(first())
.subscribe((result) => (this.groups = result.results))
}
ngOnInit(): void {
@@ -127,14 +101,15 @@ export class UserEditDialogComponent
if (!groupsVal) return []
else
return groupsVal.flatMap(
(id) => this.groups?.find((g) => g.id == id)?.permissions
(id) => this.groups()?.find((g) => g.id == id)?.permissions
)
}
save(): void {
this.passwordIsSet =
this.passwordIsSet.set(
this.objectForm.get('password').value?.toString().replaceAll('*', '')
.length > 0
)
super.save()
}
@@ -143,13 +118,13 @@ export class UserEditDialogComponent
}
deactivateTotp() {
this.totpLoading = true
this.totpLoading.set(true)
;(this.service as UserService)
.deactivateTotp(this.object)
.pipe(first())
.subscribe({
next: (result) => {
this.totpLoading = false
this.totpLoading.set(false)
if (result) {
this.toastService.showInfo($localize`Totp deactivated`)
this.object.is_mfa_enabled = false
@@ -158,7 +133,7 @@ export class UserEditDialogComponent
}
},
error: (e) => {
this.totpLoading = false
this.totpLoading.set(false)
this.toastService.showError($localize`Totp deactivation failed`, e)
},
})
@@ -3,7 +3,7 @@
<ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled"
clearable="true"
[items]="groups"
[items]="groups()"
multiple="true"
bindLabel="name"
bindValue="id"
@@ -1,11 +1,12 @@
import { Component, forwardRef, inject, signal } from '@angular/core'
import { Component, forwardRef, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { NgSelectComponent } from '@ng-select/ng-select'
import { first } from 'rxjs/operators'
import { map } from 'rxjs/operators'
import { Group } from 'src/app/data/group'
import { GroupService } from 'src/app/services/rest/group.service'
import { AbstractInputComponent } from '../../abstract-input'
@@ -24,23 +25,9 @@ import { AbstractInputComponent } from '../../abstract-input'
imports: [NgSelectComponent, FormsModule, ReactiveFormsModule],
})
export class PermissionsGroupComponent extends AbstractInputComponent<Group> {
private groupsSignal = signal<Group[]>(undefined)
get groups(): Group[] {
return this.groupsSignal()
}
set groups(groups: Group[]) {
this.groupsSignal.set(groups)
}
constructor() {
const groupService = inject(GroupService)
super()
groupService
.listAll()
.pipe(first())
.subscribe((result) => (this.groups = result.results))
}
private readonly groupService = inject(GroupService)
readonly groups = toSignal(
this.groupService.listAll().pipe(map((result) => result.results)),
{ initialValue: undefined as Group[] }
)
}
@@ -3,7 +3,7 @@
<ng-select name="inputId" [(ngModel)]="value"
[disabled]="disabled"
clearable="true"
[items]="users"
[items]="users()"
multiple="true"
bindLabel="username"
bindValue="id"
@@ -1,11 +1,12 @@
import { Component, forwardRef, inject, signal } from '@angular/core'
import { Component, forwardRef, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import {
FormsModule,
NG_VALUE_ACCESSOR,
ReactiveFormsModule,
} from '@angular/forms'
import { NgSelectComponent } from '@ng-select/ng-select'
import { first } from 'rxjs/operators'
import { map } from 'rxjs/operators'
import { User } from 'src/app/data/user'
import { UserService } from 'src/app/services/rest/user.service'
import { AbstractInputComponent } from '../../abstract-input'
@@ -24,23 +25,9 @@ import { AbstractInputComponent } from '../../abstract-input'
imports: [NgSelectComponent, FormsModule, ReactiveFormsModule],
})
export class PermissionsUserComponent extends AbstractInputComponent<User[]> {
private usersSignal = signal<User[]>(undefined)
get users(): User[] {
return this.usersSignal()
}
set users(users: User[]) {
this.usersSignal.set(users)
}
constructor() {
const userService = inject(UserService)
super()
userService
.listAll()
.pipe(first())
.subscribe((result) => (this.users = result.results))
}
private readonly userService = inject(UserService)
readonly users = toSignal(
this.userService.listAll().pipe(map((result) => result.results)),
{ initialValue: undefined as User[] }
)
}
@@ -146,8 +146,8 @@ describe('PDFEditorComponent', () => {
const previewSpy = jest
.spyOn(documentService, 'getPreviewUrl')
.mockReturnValue('preview-version')
component.documentID = 3
component.versionID = 10
component.documentID.set(3)
component.versionID.set(10)
expect(component.pdfSrc).toBe('preview-version')
expect(previewSpy).toHaveBeenCalledWith(3, false, 10)
@@ -45,24 +45,8 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
private readonly settingsService = inject(SettingsService)
activeModal: NgbActiveModal = inject(NgbActiveModal)
private documentIDSignal = signal<number>(undefined)
private versionIDSignal = signal<number>(undefined)
get documentID(): number {
return this.documentIDSignal()
}
set documentID(documentID: number) {
this.documentIDSignal.set(documentID)
}
get versionID(): number {
return this.versionIDSignal()
}
set versionID(versionID: number) {
this.versionIDSignal.set(versionID)
}
readonly documentID = signal<number>(undefined)
readonly versionID = signal<number>(undefined)
pages: PageOperation[] = []
totalPages = 0
@@ -74,9 +58,9 @@ export class PDFEditorComponent extends ConfirmDialogComponent {
get pdfSrc(): string {
return this.documentService.getPreviewUrl(
this.documentID,
this.documentID(),
false,
this.versionID
this.versionID()
)
}
@@ -8,18 +8,18 @@
<div>
<p class="mb-1">
<ng-container i18n>Selected documents:</ng-container>
{{ selectionCount }}
{{ selectionCount() }}
</p>
@if (documentPreview.length > 0) {
@if (documentPreview().length > 0) {
<ul class="list-unstyled small mb-0">
@for (doc of documentPreview; track doc.id) {
@for (doc of documentPreview(); track doc.id) {
<li>
<strong>{{ doc.title | documentTitle }}</strong>
</li>
}
@if (selectionCount > documentPreview.length) {
@if (selectionCount() > documentPreview().length) {
<li>
<ng-container i18n>+ {{ selectionCount - documentPreview.length }} more…</ng-container>
<ng-container i18n>+ {{ selectionCount() - documentPreview().length }} more…</ng-container>
</li>
}
</ul>
@@ -72,10 +72,10 @@
type="button"
(click)="copy(createdBundle)"
>
@if (copied) {
@if (copied()) {
<i-bs name="clipboard-check"></i-bs>
}
@if (!copied) {
@if (!copied()) {
<i-bs name="clipboard"></i-bs>
}
<span class="visually-hidden" i18n>Copy link</span>
@@ -118,7 +118,7 @@
type="button"
class="btn btn-primary btn-sm d-inline-flex align-items-center gap-2 text-nowrap"
(click)="submit()"
[disabled]="loading || !buttonsEnabled">
[disabled]="loading() || !buttonsEnabled">
@if (loading()) {
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
}
@@ -58,10 +58,10 @@ describe('ShareLinkBundleDialogComponent', () => {
it('builds payload and emits confirm on submit', () => {
const confirmSpy = jest.spyOn(component.confirmClicked, 'emit')
component.documents = [
component.setDocuments([
{ id: 1, title: 'Doc 1' } as any,
{ id: 2, title: 'Doc 2' } as any,
]
])
component.form.setValue({
shareArchiveVersion: false,
expirationDays: 3,
@@ -101,11 +101,11 @@ describe('ShareLinkBundleDialogComponent', () => {
const docs = Array.from({ length: 12 }).map((_, index) => ({
id: index + 1,
}))
component.documents = docs as any
component.setDocuments(docs as any)
expect(component.selectionCount).toBe(12)
expect(component.documentPreview).toHaveLength(10)
expect(component.documentPreview[0].id).toBe(1)
expect(component.selectionCount()).toBe(12)
expect(component.documentPreview()).toHaveLength(10)
expect(component.documentPreview()[0].id).toBe(1)
})
it('copies share link and resets state after timeout', fakeAsync(() => {
@@ -118,11 +118,11 @@ describe('ShareLinkBundleDialogComponent', () => {
component.copy(bundle)
expect(copySpy).toHaveBeenCalledWith(component.getShareUrl(bundle))
expect(component.copied).toBe(true)
expect(component.copied()).toBe(true)
expect(toastService.showInfo).toHaveBeenCalled()
tick(3000)
expect(component.copied).toBe(false)
expect(component.copied()).toBe(false)
}))
it('generates share URLs based on API base URL', () => {
@@ -1,6 +1,6 @@
import { Clipboard } from '@angular/cdk/clipboard'
import { CommonModule } from '@angular/common'
import { Component, Input, inject, signal } from '@angular/core'
import { Component, inject, signal } from '@angular/core'
import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Document } from 'src/app/data/document'
@@ -38,26 +38,10 @@ export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent {
private readonly clipboard = inject(Clipboard)
private readonly toastService = inject(ToastService)
private documentsSignal = signal<Document[]>([])
private selectionCountSignal = signal(0)
private documentPreviewSignal = signal<Document[]>([])
private copiedSignal = signal(false)
get selectionCount(): number {
return this.selectionCountSignal()
}
get documentPreview(): Document[] {
return this.documentPreviewSignal()
}
get copied(): boolean {
return this.copiedSignal()
}
set copied(copied: boolean) {
this.copiedSignal.set(copied)
}
readonly documents = signal<Document[]>([])
readonly selectionCount = signal(0)
readonly documentPreview = signal<Document[]>([])
readonly copied = signal(false)
form: FormGroup = this.formBuilder.group({
shareArchiveVersion: true,
@@ -78,18 +62,17 @@ export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent {
this.btnCaption = $localize`Create link`
}
@Input()
set documents(docs: Document[]) {
setDocuments(docs: Document[]) {
const documents = docs.concat()
this.documentsSignal.set(documents)
this.selectionCountSignal.set(documents.length)
this.documentPreviewSignal.set(documents.slice(0, 10))
this.documents.set(documents)
this.selectionCount.set(documents.length)
this.documentPreview.set(documents.slice(0, 10))
}
submit() {
if (this.createdBundle) return
this.payload = {
document_ids: this.documentsSignal().map((doc) => doc.id),
document_ids: this.documents().map((doc) => doc.id),
file_version: this.form.value.shareArchiveVersion
? FileVersion.Archive
: FileVersion.Original,
@@ -109,10 +92,10 @@ export class ShareLinkBundleDialogComponent extends ConfirmDialogComponent {
copy(bundle: ShareLinkBundleSummary): void {
const success = this.clipboard.copy(this.getShareUrl(bundle))
if (success) {
this.copied = true
this.copied.set(true)
this.toastService.showInfo($localize`Share link copied to clipboard.`)
setTimeout(() => {
this.copied = false
this.copied.set(false)
}, 3000)
}
}
@@ -10,21 +10,21 @@
<span i18n>Loading share link bundles…</span>
</div>
}
@if (!loading && error) {
@if (!loading() && error()) {
<div class="alert alert-danger mb-0" role="alert">
{{ error }}
{{ error() }}
</div>
}
@if (!loading && !error) {
@if (!loading() && !error()) {
<div class="d-flex justify-content-between align-items-center mb-2">
<p class="mb-0 text-muted small">
<ng-container i18n>Status updates every few seconds while bundles are being prepared.</ng-container>
</p>
</div>
@if (bundles.length === 0) {
@if (bundles().length === 0) {
<p class="mb-0 text-muted fst-italic" i18n>No share link bundles currently exist.</p>
}
@if (bundles.length > 0) {
@if (bundles().length > 0) {
<div class="table-responsive">
<table class="table table-sm align-middle mb-0">
<thead>
@@ -39,7 +39,7 @@
</tr>
</thead>
<tbody>
@for (bundle of bundles; track bundle.id) {
@for (bundle of bundles(); track bundle.id) {
<tr>
<td>
<div>{{ bundle.created | date: 'short' }}</div>
@@ -113,10 +113,10 @@
title="Copy share link"
i18n-title
>
@if (copiedSlug === bundle.slug) {
@if (copiedSlug() === bundle.slug) {
<i-bs name="clipboard-check"></i-bs>
}
@if (copiedSlug !== bundle.slug) {
@if (copiedSlug() !== bundle.slug) {
<i-bs name="clipboard"></i-bs>
}
<span class="visually-hidden" i18n>Copy share link</span>
@@ -96,9 +96,9 @@ describe('ShareLinkBundleManageDialogComponent', () => {
tick()
expect(service.listAllBundles).toHaveBeenCalledTimes(1)
expect(component.bundles).toEqual(bundles)
expect(component.bundles()).toEqual(bundles)
expect(component.loading()).toBe(false)
expect(component.error).toBeNull()
expect(component.error()).toBeNull()
tick(5000)
expect(service.listAllBundles).toHaveBeenCalledTimes(2)
@@ -113,7 +113,7 @@ describe('ShareLinkBundleManageDialogComponent', () => {
fixture.detectChanges()
tick()
expect(component.error).toContain('Failed to load share link bundles.')
expect(component.error()).toContain('Failed to load share link bundles.')
expect(toastService.showError).toHaveBeenCalled()
expect(component.loading()).toBe(false)
@@ -135,11 +135,11 @@ describe('ShareLinkBundleManageDialogComponent', () => {
expect(clipboard.copy).toHaveBeenCalledWith(
component.getShareUrl(readyBundle)
)
expect(component.copiedSlug).toBe('ready-slug')
expect(component.copiedSlug()).toBe('ready-slug')
expect(toastService.showInfo).toHaveBeenCalled()
tick(3000)
expect(component.copiedSlug).toBeNull()
expect(component.copiedSlug()).toBeNull()
}))
it('ignores copy requests for non-ready bundles', fakeAsync(() => {
@@ -190,12 +190,12 @@ describe('ShareLinkBundleManageDialogComponent', () => {
fixture.detectChanges()
tick()
component.bundles = [sampleBundle()]
component.retry(component.bundles[0])
component.bundles.set([sampleBundle()])
component.retry(component.bundles()[0])
tick()
expect(service.rebuildBundle).toHaveBeenCalledWith(updated.id)
expect(component.bundles[0].status).toBe(ShareLinkBundleStatus.Ready)
expect(component.bundles()[0].status).toBe(ShareLinkBundleStatus.Ready)
expect(toastService.showInfo).toHaveBeenCalled()
}))
@@ -208,11 +208,11 @@ describe('ShareLinkBundleManageDialogComponent', () => {
fixture.detectChanges()
tick()
component.bundles = [sampleBundle()]
component.bundles.set([sampleBundle()])
component.retry({ id: 99 } as ShareLinkBundleSummary)
tick()
expect(component.bundles.find((bundle) => bundle.id === 99)).toBeTruthy()
expect(component.bundles().find((bundle) => bundle.id === 99)).toBeTruthy()
}))
it('handles retry errors', fakeAsync(() => {
@@ -40,33 +40,9 @@ export class ShareLinkBundleManageDialogComponent
private readonly clipboard = inject(Clipboard)
title = $localize`Share link bundles`
private bundlesSignal = signal<ShareLinkBundleSummary[]>([])
private errorSignal = signal<string | null>(null)
private copiedSlugSignal = signal<string | null>(null)
get bundles(): ShareLinkBundleSummary[] {
return this.bundlesSignal()
}
set bundles(bundles: ShareLinkBundleSummary[]) {
this.bundlesSignal.set(bundles)
}
get error(): string | null {
return this.errorSignal()
}
set error(error: string | null) {
this.errorSignal.set(error)
}
get copiedSlug(): string | null {
return this.copiedSlugSignal()
}
set copiedSlug(copiedSlug: string | null) {
this.copiedSlugSignal.set(copiedSlug)
}
readonly bundles = signal<ShareLinkBundleSummary[]>([])
readonly error = signal<string | null>(null)
readonly copiedSlug = signal<string | null>(null)
readonly statuses = ShareLinkBundleStatus
readonly fileVersions = FileVersion
@@ -80,13 +56,13 @@ export class ShareLinkBundleManageDialogComponent
if (!silent) {
this.loading.set(true)
}
this.error = null
this.error.set(null)
return this.shareLinkBundleService.listAllBundles().pipe(
catchError((error) => {
if (!silent) {
this.loading.set(false)
}
this.error = $localize`Failed to load share link bundles.`
this.error.set($localize`Failed to load share link bundles.`)
this.toastService.showError(
$localize`Error retrieving share link bundles.`,
error
@@ -99,8 +75,8 @@ export class ShareLinkBundleManageDialogComponent
)
.subscribe((results) => {
if (results) {
this.bundles = results
this.copiedSlug = null
this.bundles.set(results)
this.copiedSlug.set(null)
}
this.loading.set(false)
})
@@ -128,16 +104,16 @@ export class ShareLinkBundleManageDialogComponent
}
const success = this.clipboard.copy(this.getShareUrl(bundle))
if (success) {
this.copiedSlug = bundle.slug
this.copiedSlug.set(bundle.slug)
setTimeout(() => {
this.copiedSlug = null
this.copiedSlug.set(null)
}, 3000)
this.toastService.showInfo($localize`Share link copied to clipboard.`)
}
}
delete(bundle: ShareLinkBundleSummary): void {
this.error = null
this.error.set(null)
this.loading.set(true)
this.shareLinkBundleService.delete(bundle).subscribe({
next: () => {
@@ -155,7 +131,7 @@ export class ShareLinkBundleManageDialogComponent
}
retry(bundle: ShareLinkBundleSummary): void {
this.error = null
this.error.set(null)
this.shareLinkBundleService.rebuildBundle(bundle.id).subscribe({
next: (updated) => {
this.toastService.showInfo(
@@ -182,15 +158,16 @@ export class ShareLinkBundleManageDialogComponent
}
private replaceBundle(updated: ShareLinkBundleSummary): void {
const index = this.bundles.findIndex((bundle) => bundle.id === updated.id)
const bundles = this.bundles()
const index = bundles.findIndex((bundle) => bundle.id === updated.id)
if (index >= 0) {
this.bundles = [
...this.bundles.slice(0, index),
this.bundles.set([
...bundles.slice(0, index),
updated,
...this.bundles.slice(index + 1),
]
...bundles.slice(index + 1),
])
} else {
this.bundles = [updated, ...this.bundles]
this.bundles.set([updated, ...bundles])
}
}
@@ -1697,8 +1697,8 @@ describe('DocumentDetailComponent', () => {
component.selectedVersionId = 10
component.editPdf()
expect(modal).not.toBeUndefined()
modal.componentInstance.documentID = doc.id
expect(modal.componentInstance.versionID).toBe(10)
modal.componentInstance.documentID.set(doc.id)
expect(modal.componentInstance.versionID()).toBe(10)
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: false }]
modal.componentInstance.confirm()
let req = httpTestingController.expectOne(
@@ -1716,7 +1716,7 @@ describe('DocumentDetailComponent', () => {
expect(errorSpy).toHaveBeenCalled()
component.editPdf()
modal.componentInstance.documentID = doc.id
modal.componentInstance.documentID.set(doc.id)
modal.componentInstance.pages = [{ page: 1, rotate: 0, splitAfter: true }]
modal.componentInstance.deleteOriginal = true
modal.componentInstance.confirm()
@@ -1994,8 +1994,8 @@ export class DocumentDetailComponent
const sourceDocumentId = this.selectedVersionId ?? this.document.id
modal.componentInstance.title = $localize`PDF Editor`
modal.componentInstance.btnCaption = $localize`Proceed`
modal.componentInstance.documentID = this.document.id
modal.componentInstance.versionID = sourceDocumentId
modal.componentInstance.documentID.set(this.document.id)
modal.componentInstance.versionID.set(sourceDocumentId)
modal.componentInstance.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
@@ -1146,7 +1146,7 @@ describe('BulkEditorComponent', () => {
fixture.detectChanges()
component.mergeSelected()
expect(modal).not.toBeUndefined()
modal.componentInstance.metadataDocumentID = 3
modal.componentInstance.metadataDocumentID.set(3)
modal.componentInstance.confirm()
let req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/merge/`
@@ -1164,7 +1164,7 @@ describe('BulkEditorComponent', () => {
) // listAllFilteredIds
// Test with Delete Originals enabled
modal.componentInstance.deleteOriginals = true
modal.componentInstance.deleteOriginals.set(true)
modal.componentInstance.confirm()
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/merge/`
@@ -1184,8 +1184,8 @@ describe('BulkEditorComponent', () => {
expect(documentListViewService.selected.size).toEqual(0)
// Test with archiveFallback enabled
modal.componentInstance.deleteOriginals = false
modal.componentInstance.archiveFallback = true
modal.componentInstance.deleteOriginals.set(false)
modal.componentInstance.archiveFallback.set(true)
modal.componentInstance.confirm()
req = httpTestingController.expectOne(
`${environment.apiBaseUrl}documents/merge/`
@@ -1629,6 +1629,9 @@ describe('BulkEditorComponent', () => {
close: jest.fn(),
componentInstance: {
documents: [],
setDocuments(docs) {
this.documents = docs
},
confirmClicked,
payload: {
document_ids: [5, 7],
@@ -1637,7 +1640,7 @@ describe('BulkEditorComponent', () => {
},
loading: signal(false),
buttonsEnabled: true,
copied: false,
copied: signal(false),
},
}
@@ -1698,6 +1701,9 @@ describe('BulkEditorComponent', () => {
const modalRef: Partial<NgbModalRef> = {
componentInstance: {
documents: [],
setDocuments(docs) {
this.documents = docs
},
confirmClicked,
payload: {
document_ids: [9],
@@ -956,24 +956,24 @@ export class BulkEditorComponent
mergeDialog.title = $localize`Merge confirm`
mergeDialog.messageBold = $localize`This operation will merge ${this.getSelectionSize()} selected documents into a new document.`
mergeDialog.btnCaption = $localize`Proceed`
mergeDialog.documentIDs = Array.from(this.list.selected)
mergeDialog.documentIDs.set(Array.from(this.list.selected))
mergeDialog.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
const args: MergeDocumentsRequest = {}
if (mergeDialog.metadataDocumentID > -1) {
args.metadata_document_id = mergeDialog.metadataDocumentID
if (mergeDialog.metadataDocumentID() > -1) {
args.metadata_document_id = mergeDialog.metadataDocumentID()
}
if (mergeDialog.deleteOriginals) {
if (mergeDialog.deleteOriginals()) {
args.delete_originals = true
}
if (mergeDialog.archiveFallback) {
if (mergeDialog.archiveFallback()) {
args.archive_fallback = true
}
mergeDialog.buttonsEnabled = false
this.executeDocumentAction(
modal,
this.documentService.mergeDocuments(mergeDialog.documentIDs, args),
this.documentService.mergeDocuments(mergeDialog.documentIDs(), args),
{ deleteOriginals: !!args.delete_originals }
)
this.toastService.showInfo(
@@ -1029,7 +1029,7 @@ export class BulkEditorComponent
const selectedDocuments = this.list.documents.filter((d) =>
this.list.selected.has(d.id)
)
dialog.documents = selectedDocuments
dialog.setDocuments(selectedDocuments)
dialog.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
@@ -1043,7 +1043,7 @@ export class BulkEditorComponent
dialog.loading.set(false)
dialog.buttonsEnabled = false
dialog.createdBundle = result
dialog.copied = false
dialog.copied.set(false)
dialog.payload = null
dialog.onOpenManage = () => {
modal.close()
@@ -730,7 +730,9 @@ describe('DocumentListComponent', () => {
showInSideBar: true,
})
expect(updateVisibilitySpy).not.toHaveBeenCalled()
expect(openModal.componentInstance.error).toEqual({ filter_rules: ['11'] })
expect(openModal.componentInstance.error()).toEqual({
filter_rules: ['11'],
})
})
it('should detect saved view changes', () => {
@@ -449,9 +449,11 @@ export class DocumentListComponent
let modal = this.modalService.open(SaveViewConfigDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.defaultName = this.filterEditor.generateFilterName()
modal.componentInstance.setDefaultName(
this.filterEditor.generateFilterName()
)
modal.componentInstance.saveClicked.pipe(first()).subscribe((formValue) => {
modal.componentInstance.buttonsEnabled = false
modal.componentInstance.buttonsEnabled.set(false)
let savedView: SavedView = {
name: formValue.name,
filter_rules: this.list.filterRules,
@@ -502,8 +504,8 @@ export class DocumentListComponent
if (error.filter_rules) {
error.filter_rules = error.filter_rules.map((r) => r.value)
}
modal.componentInstance.error = error
modal.componentInstance.buttonsEnabled = true
modal.componentInstance.error.set(error)
modal.componentInstance.buttonsEnabled.set(true)
},
})
})
@@ -1,24 +1,24 @@
<form [formGroup]="saveViewConfigForm" (ngSubmit)="save()">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title" i18n>Save current view</h4>
<button type="button" [disabled]="!closeEnabled" class="btn-close" aria-label="Close" (click)="cancel()">
<button type="button" [disabled]="!closeEnabled()" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error?.name" autocomplete="off"></pngx-input-text>
<pngx-input-text i18n-title title="Name" formControlName="name" [error]="error()?.name" autocomplete="off"></pngx-input-text>
<pngx-input-check i18n-title title="Show in sidebar" formControlName="showInSideBar"></pngx-input-check>
<pngx-input-check i18n-title title="Show on dashboard" formControlName="showOnDashboard"></pngx-input-check>
<pngx-permissions-form accordion="true" formControlName="permissions_form"></pngx-permissions-form>
@if (error?.filter_rules) {
@if (error()?.filter_rules) {
<div class="alert alert-danger" role="alert">
<h6 class="alert-heading" i18n>Filter rules error occurred while saving this view</h6>
<ng-container i18n>The error returned was</ng-container>:<br/>
{{ error.filter_rules }}
{{ error().filter_rules }}
</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="!buttonsEnabled">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="!buttonsEnabled">Save</button>
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="!buttonsEnabled()">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="!buttonsEnabled()">Save</button>
</div>
</form>
@@ -63,9 +63,9 @@ describe('SaveViewConfigDialogComponent', () => {
const name = 'Tag: Inbox'
let result
component.saveClicked.subscribe((saveResult) => (result = saveResult))
component.defaultName = name
component.setDefaultName(name)
component.save()
expect(component.defaultName).toEqual(name)
expect(component.defaultName()).toEqual(name)
expect(result).toEqual({
name,
showInSideBar: false,
@@ -1,7 +1,6 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
inject,
@@ -33,49 +32,18 @@ import { TextComponent } from '../../common/input/text/text.component'
})
export class SaveViewConfigDialogComponent implements OnInit {
private modal = inject(NgbActiveModal)
private errorSignal = signal(undefined)
private buttonsEnabledSignal = signal(true)
private defaultNameSignal = signal('')
private closeEnabledSignal = signal(false)
readonly error = signal(undefined)
readonly buttonsEnabled = signal(true)
readonly defaultName = signal('')
readonly closeEnabled = signal(false)
@Output()
public saveClicked = new EventEmitter()
@Input()
get error() {
return this.errorSignal()
}
set error(error) {
this.errorSignal.set(error)
}
@Input()
get buttonsEnabled(): boolean {
return this.buttonsEnabledSignal()
}
set buttonsEnabled(buttonsEnabled: boolean) {
this.buttonsEnabledSignal.set(buttonsEnabled)
}
get closeEnabled(): boolean {
return this.closeEnabledSignal()
}
set closeEnabled(closeEnabled: boolean) {
this.closeEnabledSignal.set(closeEnabled)
}
users: User[]
get defaultName() {
return this.defaultNameSignal()
}
@Input()
set defaultName(value: string) {
this.defaultNameSignal.set(value)
setDefaultName(value: string) {
this.defaultName.set(value)
this.saveViewConfigForm.patchValue({ name: value })
}
@@ -89,7 +57,7 @@ export class SaveViewConfigDialogComponent implements OnInit {
ngOnInit(): void {
// wait to enable close button so it doesn't steal focus from input since its the first clickable element in the DOM
setTimeout(() => {
this.closeEnabled = true
this.closeEnabled.set(true)
})
}
@@ -1,4 +1,4 @@
@if (notes) {
@if (notes()) {
<div>
<form [formGroup]="noteForm" class="needs-validation mt-3" *pngxIfPermissions="{ action: PermissionAction.Add, type: PermissionType.Note }" novalidate>
<div class="form-group">
@@ -8,14 +8,14 @@
</div>
</div>
<div class="form-group mt-2 d-flex justify-content-end align-items-center">
@if (networkActive) {
@if (networkActive()) {
<div class="spinner-border spinner-border-sm fw-normal me-auto" role="status"></div>
}
<button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive || addDisabled" (click)="addNote()" i18n>Add note</button>
<button type="button" class="btn btn-primary btn-sm" [disabled]="networkActive() || addDisabled()" (click)="addNote()" i18n>Add note</button>
</div>
</form>
<hr>
@for (note of notes; track note) {
@for (note of notes(); track note) {
<div class="card border mb-3">
<div class="card-body text-dark">
<p class="card-text">{{note.note}}</p>
@@ -110,7 +110,7 @@ describe('DocumentNotesComponent', () => {
})
it('should display notes with user name / username', () => {
component.notes = notes
component.notes.set(notes)
fixture.detectChanges()
expect(fixture.debugElement.nativeElement.textContent).toContain(
notes[0].note
@@ -154,7 +154,7 @@ describe('DocumentNotesComponent', () => {
})
it('should support note entry, show error if fails', () => {
component.documentId = 12
component.documentId.set(12)
const note = 'This is the new note.'
const noteTextArea = fixture.debugElement.query(By.css('textarea'))
noteTextArea.nativeElement.value = note
@@ -177,7 +177,7 @@ describe('DocumentNotesComponent', () => {
})
it('should support note save on ctrl+Enter', () => {
component.documentId = 12
component.documentId.set(12)
const note = 'This is the new note.'
const noteTextArea = fixture.debugElement.query(By.css('textarea'))
noteTextArea.nativeElement.value = note
@@ -189,8 +189,8 @@ describe('DocumentNotesComponent', () => {
})
it('should support delete note, show error if fails', () => {
component.documentId = 12
component.notes = notes
component.documentId.set(12)
component.notes.set(notes)
fixture.detectChanges()
const deleteButton = fixture.debugElement.queryAll(By.css('button'))[1] // 0 is add button
const deleteSpy = jest.spyOn(notesService, 'deleteNote')
@@ -1,9 +1,10 @@
import {
Component,
EventEmitter,
Input,
Output,
inject,
input,
model,
signal,
} from '@angular/core'
import {
@@ -38,52 +39,17 @@ export class DocumentNotesComponent extends ComponentWithPermissions {
private notesService = inject(DocumentNotesService)
private toastService = inject(ToastService)
private usersService = inject(UserService)
private documentIdSignal = signal<number>(undefined)
private notesSignal = signal<DocumentNote[]>([])
private addDisabledSignal = signal(false)
private networkActiveSignal = signal(false)
readonly documentId = model<number>(undefined)
readonly notes = model<DocumentNote[]>([])
readonly addDisabled = input(false)
readonly networkActive = signal(false)
noteForm: FormGroup = new FormGroup({
newNote: new FormControl(''),
})
get networkActive(): boolean {
return this.networkActiveSignal()
}
set networkActive(networkActive: boolean) {
this.networkActiveSignal.set(networkActive)
}
newNoteError: boolean = false
@Input()
get documentId(): number {
return this.documentIdSignal()
}
set documentId(documentId: number) {
this.documentIdSignal.set(documentId)
}
@Input()
get notes(): DocumentNote[] {
return this.notesSignal()
}
set notes(notes: DocumentNote[]) {
this.notesSignal.set(notes)
}
@Input()
get addDisabled(): boolean {
return this.addDisabledSignal()
}
set addDisabled(addDisabled: boolean) {
this.addDisabledSignal.set(addDisabled)
}
@Output()
updated: EventEmitter<DocumentNote[]> = new EventEmitter()
users: User[]
@@ -104,30 +70,30 @@ export class DocumentNotesComponent extends ComponentWithPermissions {
return
}
this.newNoteError = false
this.networkActive = true
this.notesService.addNote(this.documentId, note).subscribe({
this.networkActive.set(true)
this.notesService.addNote(this.documentId(), note).subscribe({
next: (result) => {
this.notes = result
this.notes.set(result)
this.noteForm.get('newNote').reset()
this.networkActive = false
this.updated.emit(this.notes)
this.networkActive.set(false)
this.updated.emit(this.notes())
},
error: (e) => {
this.networkActive = false
this.networkActive.set(false)
this.toastService.showError($localize`Error saving note`, e)
},
})
}
deleteNote(noteId: number) {
this.notesService.deleteNote(this.documentId, noteId).subscribe({
this.notesService.deleteNote(this.documentId(), noteId).subscribe({
next: (result) => {
this.notes = result
this.networkActive = false
this.updated.emit(this.notes)
this.notes.set(result)
this.networkActive.set(false)
this.updated.emit(this.notes())
},
error: (e) => {
this.networkActive = false
this.networkActive.set(false)
this.toastService.showError($localize`Error deleting note`, e)
},
})
@@ -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 >
@@ -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
)
@@ -133,19 +133,11 @@ export class DocumentAttributesComponent implements OnInit, OnDestroy {
@ViewChild('activeOutlet', { read: NgComponentOutlet })
set activeOutlet(outlet: NgComponentOutlet | undefined) {
this.activeComponentSignal.set(outlet?.componentInstance ?? null)
this.activeComponent.set(outlet?.componentInstance ?? null)
}
private activeComponentSignal = signal<unknown>(null)
private activeNavIDSignal = signal<number>(null)
get activeNavID(): number {
return this.activeNavIDSignal()
}
set activeNavID(activeNavID: number) {
this.activeNavIDSignal.set(activeNavID)
}
readonly activeComponent = signal<unknown>(null)
readonly activeNavID = signal<number>(null)
get visibleSections(): DocumentAttributesSection[] {
return this.sections.filter((section) =>
@@ -158,8 +150,9 @@ export class DocumentAttributesComponent implements OnInit, OnDestroy {
get activeSection(): DocumentAttributesSection | null {
return (
this.visibleSections.find((section) => section.id === this.activeNavID) ??
null
this.visibleSections.find(
(section) => section.id === this.activeNavID()
) ?? null
)
}
@@ -168,14 +161,14 @@ export class DocumentAttributesComponent implements OnInit, OnDestroy {
this.activeSection?.kind !== DocumentAttributesSectionKind.ManagementList
)
return null
const instance = this.activeComponentSignal()
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.activeComponentSignal()
const instance = this.activeComponent()
return instance instanceof CustomFieldsComponent ? instance : null
}
@@ -208,13 +201,13 @@ export class DocumentAttributesComponent implements OnInit, OnDestroy {
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 }
)
}
@@ -41,7 +41,7 @@
</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="col d-flex align-items-center">
@@ -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>
@@ -186,7 +186,7 @@
</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>
@@ -56,13 +56,10 @@ export class MailComponent
public MailAccountType = MailAccountType
private mailAccountsSignal = signal<MailAccount[]>([])
readonly mailAccounts = signal<MailAccount[]>([])
public get mailAccounts() {
return this.mailAccountsSignal()
}
private set mailAccounts(accounts: MailAccount[]) {
this.mailAccountsSignal.set(accounts)
private setMailAccounts(accounts: MailAccount[]) {
this.mailAccounts.set(accounts)
this.mailAccountsById.set(
new Map(accounts.map((account) => [account.id, account]))
)
@@ -94,12 +91,12 @@ 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
)
)
@@ -140,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
)
)
@@ -179,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
@@ -210,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) => {
@@ -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,7 +65,7 @@
</li>
}
@if (savedViews && savedViews.length === 0) {
@if (savedViews() && savedViews().length === 0) {
<li class="list-group-item">
<div i18n>No saved views defined.</div>
</li>
@@ -56,7 +56,7 @@ export class SavedViewsComponent
DisplayMode = DisplayMode
private savedViewsSignal = signal<SavedView[]>(undefined)
readonly savedViews = signal<SavedView[]>(undefined)
private savedViewsGroup = new FormGroup({})
public savedViewsForm: FormGroup = new FormGroup({
savedViews: this.savedViewsGroup,
@@ -69,14 +69,6 @@ export class SavedViewsComponent
return this.settings.allDisplayFields()
}
public get savedViews(): SavedView[] {
return this.savedViewsSignal()
}
public set savedViews(savedViews: SavedView[]) {
this.savedViewsSignal.set(savedViews)
}
constructor() {
super()
this.settings.organizingSidebarSavedViews.set(true)
@@ -91,7 +83,7 @@ export class SavedViewsComponent
this.savedViewService
.list(null, null, null, false, { full_perms: true })
.subscribe((r) => {
this.savedViews = r.results
this.savedViews.set(r.results)
this.initialize()
})
}
@@ -109,7 +101,7 @@ export class SavedViewsComponent
savedViews: {},
}
for (let view of this.savedViews) {
for (let view of this.savedViews()) {
storeData.savedViews[view.id.toString()] = {
id: view.id,
name: view.name,
@@ -156,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.`
)