Chorehancement: update to Angular v22, 'zoneless' / 'reactive' (#13114)

This commit is contained in:
shamoon
2026-07-10 00:42:16 -07:00
committed by GitHub
parent f244442c65
commit 106b41a15c
213 changed files with 5363 additions and 5842 deletions
@@ -4,7 +4,7 @@ import {
HttpTestingController,
provideHttpClientTesting,
} from '@angular/common/http/testing'
import { EventEmitter } from '@angular/core'
import { EventEmitter, signal } from '@angular/core'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { By } from '@angular/platform-browser'
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
@@ -38,7 +38,6 @@ import { environment } from 'src/environments/environment'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { FilterableDropdownComponent } from '../../common/filterable-dropdown/filterable-dropdown.component'
@@ -1146,7 +1145,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 +1163,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 +1183,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/`
@@ -1339,7 +1338,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newTag),
},
@@ -1382,7 +1381,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newCorrespondent),
},
@@ -1431,7 +1430,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newDocumentType),
},
@@ -1477,7 +1476,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newStoragePath),
},
@@ -1531,7 +1530,7 @@ describe('BulkEditorComponent', () => {
const modalInstance = {
componentInstance: {
dialogMode: EditDialogMode.CREATE,
dialogMode: { set: jest.fn() },
object: { name },
succeeded: of(newCustomField),
},
@@ -1629,15 +1628,18 @@ describe('BulkEditorComponent', () => {
close: jest.fn(),
componentInstance: {
documents: [],
setDocuments(docs) {
this.documents = docs
},
confirmClicked,
payload: {
document_ids: [5, 7],
file_version: 'archive',
expiration_days: 7,
},
loading: false,
loading: signal(false),
buttonsEnabled: true,
copied: false,
copied: signal(false),
},
}
@@ -1667,7 +1669,7 @@ describe('BulkEditorComponent', () => {
file_version: 'archive',
expiration_days: 7,
})
expect(dialogInstance.loading).toBe(false)
expect(dialogInstance.loading()).toBe(false)
expect(dialogInstance.buttonsEnabled).toBe(false)
expect(dialogInstance.createdBundle).toEqual({ id: 42 })
expect(typeof dialogInstance.onOpenManage).toBe('function')
@@ -1698,13 +1700,16 @@ describe('BulkEditorComponent', () => {
const modalRef: Partial<NgbModalRef> = {
componentInstance: {
documents: [],
setDocuments(docs) {
this.documents = docs
},
confirmClicked,
payload: {
document_ids: [9],
file_version: 'original',
expiration_days: null,
},
loading: false,
loading: signal(false),
buttonsEnabled: true,
},
}
@@ -1726,7 +1731,7 @@ describe('BulkEditorComponent', () => {
$localize`Share link bundle creation is not available yet.`,
expect.any(Error)
)
expect(dialogInstance.loading).toBe(false)
expect(dialogInstance.loading()).toBe(false)
expect(dialogInstance.buttonsEnabled).toBe(true)
openSpy.mockRestore()
})
@@ -266,7 +266,7 @@ export class BulkEditorComponent
overrideSelection?: DocumentSelectionQuery
) {
if (modal) {
modal.componentInstance.buttonsEnabled = false
this.setModalButtonsEnabled(modal, false)
}
this.documentService
.bulkEdit(overrideSelection ?? this.getSelectionQuery(), method, args)
@@ -283,7 +283,7 @@ export class BulkEditorComponent
options: { deleteOriginals?: boolean } = {}
) {
if (modal) {
modal.componentInstance.buttonsEnabled = false
this.setModalButtonsEnabled(modal, false)
}
request.pipe(first()).subscribe({
next: () => {
@@ -313,7 +313,7 @@ export class BulkEditorComponent
private handleOperationError(modal: NgbModalRef, error: any) {
if (modal) {
modal.componentInstance.buttonsEnabled = true
this.setModalButtonsEnabled(modal, true)
}
this.toastService.showError(
$localize`Error executing bulk operation`,
@@ -321,6 +321,15 @@ export class BulkEditorComponent
)
}
private setModalButtonsEnabled(modal: NgbModalRef, enabled: boolean) {
const buttonsEnabled = modal.componentInstance.buttonsEnabled
if (typeof buttonsEnabled?.set === 'function') {
buttonsEnabled.set(enabled)
} else {
modal.componentInstance.buttonsEnabled = enabled
}
}
private applySelectionData(
items: SelectionDataItem[],
selectionModel: FilterableDropdownSelectionModel
@@ -736,7 +745,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(TagEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -757,7 +766,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(CorrespondentEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -780,7 +789,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(DocumentTypeEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -801,7 +810,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(StoragePathEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -822,7 +831,7 @@ export class BulkEditorComponent
let modal = this.modalService.open(CustomFieldEditDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.dialogMode = EditDialogMode.CREATE
modal.componentInstance.dialogMode.set(EditDialogMode.CREATE)
modal.componentInstance.object = { name }
modal.componentInstance.succeeded
.pipe(
@@ -914,7 +923,7 @@ export class BulkEditorComponent
})
modal.componentInstance.confirmClicked.subscribe(
({ permissions, merge }) => {
modal.componentInstance.buttonsEnabled = false
modal.componentInstance.buttonsEnabled.set(false)
this.executeBulkEditMethod(modal, 'set_permissions', {
...permissions,
merge,
@@ -933,7 +942,7 @@ export class BulkEditorComponent
rotateDialog.messageBold = $localize`This operation will add rotated versions of the ${this.getSelectionSize()} document(s).`
rotateDialog.btnClass = 'btn-danger'
rotateDialog.btnCaption = $localize`Proceed`
rotateDialog.documentID = Array.from(this.list.selected)[0]
rotateDialog.documentID.set(Array.from(this.list.selected)[0])
rotateDialog.confirmClicked
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
@@ -956,24 +965,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,21 +1038,21 @@ 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(() => {
dialog.loading = true
dialog.loading.set(true)
dialog.buttonsEnabled = false
this.shareLinkBundleService
.createBundle(dialog.payload)
.pipe(first())
.subscribe({
next: (result) => {
dialog.loading = false
dialog.loading.set(false)
dialog.buttonsEnabled = false
dialog.createdBundle = result
dialog.copied = false
dialog.copied.set(false)
dialog.payload = null
dialog.onOpenManage = () => {
modal.close()
@@ -1054,7 +1063,7 @@ export class BulkEditorComponent
)
},
error: (error) => {
dialog.loading = false
dialog.loading.set(false)
dialog.buttonsEnabled = true
this.toastService.showError(
$localize`Share link bundle creation is not available yet.`,
@@ -1080,7 +1089,7 @@ export class BulkEditorComponent
const modal = this.modalService.open(EmailDocumentDialogComponent, {
backdrop: 'static',
})
modal.componentInstance.documentIds = Array.from(this.list.selected)
modal.componentInstance.hasArchiveVersion = allHaveArchiveVersion
modal.componentInstance.documentIds.set(Array.from(this.list.selected))
modal.componentInstance.hasArchiveVersion.set(allHaveArchiveVersion)
}
}
@@ -1,13 +1,13 @@
<div class="card document-card-large mb-3 shadow-sm bg-light placeholder-glow fade" [class.show]="show" [class.card-selected]="selected" [class.document-card]="selectable" (mouseleave)="mouseLeaveCard()">
<div class="card document-card-large mb-3 shadow-sm bg-light placeholder-glow fade" [class.show]="show()" [class.card-selected]="selected()" [class.document-card]="selectable" (mouseleave)="mouseLeaveCard()">
<div class="row g-0">
<div class="col-md-2 doc-img-container rounded-start" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit()">
@if (document) {
@if (document()) {
<img [src]="getThumbUrl()" class="card-img doc-img border-end rounded-start" [class.inverted]="getIsThumbInverted()">
<div class="border-end border-bottom bg-light document-card-check">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="smallCardCheck{{document.id}}" [checked]="selected" (click)="this.toggleSelected.emit($event)">
<label class="form-check-label" for="smallCardCheck{{document.id}}"></label>
<input type="checkbox" class="form-check-input" id="smallCardCheck{{document().id}}" [checked]="selected()" (click)="this.toggleSelected.emit($event)">
<label class="form-check-label" for="smallCardCheck{{document().id}}"></label>
</div>
</div>
} @else {
@@ -19,20 +19,20 @@
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title w-100">
@if (document) {
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
@if (document()) {
@if (displayFields().includes(DisplayField.CORRESPONDENT) && document().correspondent) {
@if (clickCorrespondent.observers.length ) {
<a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{document.correspondent | correspondentName | async}}</a>
<a title="Filter by correspondent" i18n-title (click)="clickCorrespondent.emit(document().correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{document().correspondent | correspondentName | async}}</a>
} @else {
{{document.correspondent | correspondentName | async}}
{{document().correspondent | correspondentName | async}}
}
@if (displayFields.includes(DisplayField.TITLE)) {:}
@if (displayFields().includes(DisplayField.TITLE)) {:}
}
@if (displayFields.includes(DisplayField.TITLE)) {
{{document.title | documentTitle}}
@if (displayFields().includes(DisplayField.TITLE)) {
{{document().title | documentTitle}}
}
@if (displayFields.includes(DisplayField.TAGS)) {
@for (tagID of document.tags; track tagID) {
@if (displayFields().includes(DisplayField.TAGS)) {
@for (tagID of document().tags; track tagID) {
<pngx-tag [tagID]="tagID" linkTitle="Filter by tag" i18n-linkTitle class="ms-1" (click)="clickTag.emit(tagID);$event.stopPropagation()" [clickable]="clickTag.observers.length"></pngx-tag>
}
}
@@ -42,9 +42,9 @@
</h5>
</div>
<p class="card-text">
@if (document) {
@if (document()) {
@if (hasSearchHighlights) {
<span [innerHtml]="document.__search_hit__.highlights"></span>
<span [innerHtml]="document().__search_hit__.highlights"></span>
}
@for (highlight of searchNoteHighlights; track highlight) {
<span class="d-block">
@@ -64,14 +64,14 @@
<div class="d-flex flex-column flex-md-row align-items-md-center">
<div class="btn-group">
@if (document) {
@if (document()) {
<a class="btn btn-sm btn-outline-secondary" (click)="clickMoreLike.emit()">
<i-bs name="diagram-3" class="me-1"></i-bs><span class="d-none d-md-inline" i18n>More like this</span>
</a>
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<a routerLink="/documents/{{document().id}}" class="btn btn-sm btn-outline-secondary" *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }">
<i-bs name="file-earmark-richtext" class="me-1"></i-bs><span class="d-none d-md-inline" i18n>Open</span>
</a>
<pngx-preview-popup [document]="document" #popupPreview>
<pngx-preview-popup [document]="document()" #popupPreview>
<i-bs name="eye" class="me-1"></i-bs><span class="d-none d-md-inline" i18n>View</span>
</pngx-preview-popup>
<a class="btn btn-sm btn-outline-secondary" [href]="getDownloadUrl()">
@@ -86,76 +86,76 @@
</div>
<div class="list-group list-group-horizontal border-0 card-info ms-md-auto mt-2 mt-md-0">
@if (document) {
@if (displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
<button routerLink="/documents/{{document.id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="View notes" i18n-title>
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="chat-left-text"></i-bs><small>{{document.notes.length}} Notes</small>
@if (document()) {
@if (displayFields().includes(DisplayField.NOTES) && notesEnabled && document().notes.length) {
<button routerLink="/documents/{{document().id}}/notes" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="View notes" i18n-title>
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="chat-left-text"></i-bs><small>{{document().notes.length}} Notes</small>
</button>
}
@if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
@if (displayFields().includes(DisplayField.DOCUMENT_TYPE) && document().document_type) {
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by document type" i18n-title
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{document.document_type | documentTypeName | async}}</small>
(click)="clickDocumentType.emit(document().document_type);$event.stopPropagation()">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="file-earmark"></i-bs><small>{{document().document_type | documentTypeName | async}}</small>
</button>
}
@if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
@if (displayFields().includes(DisplayField.STORAGE_PATH) && document().storage_path) {
<button type="button" class="list-group-item btn btn-sm bg-light text-dark p-1 border-0 me-2 d-flex align-items-center" title="Filter by storage path" i18n-title
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{document.storage_path | storagePathName | async}}</small>
(click)="clickStoragePath.emit(document().storage_path);$event.stopPropagation()">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="archive"></i-bs><small>{{document().storage_path | storagePathName | async}}</small>
</button>
}
@if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
@if (displayFields().includes(DisplayField.ASN) && document().archive_serial_number | isNumber) {
<div class="list-group-item me-2 bg-light text-dark p-1 border-0 d-flex align-items-center">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document.archive_serial_number}}</small>
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="upc-scan"></i-bs><small>#{{document().archive_serial_number}}</small>
</div>
}
@if (displayFields.includes(DisplayField.CREATED) || displayFields.includes(DisplayField.ADDED)) {
@if (displayFields().includes(DisplayField.CREATED) || displayFields().includes(DisplayField.ADDED)) {
<ng-template #dateTooltip>
<div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span>
<span i18n>Created: {{ document().created | customDate }}</span>
<span i18n>Added: {{ document().added | customDate }}</span>
<span i18n>Modified: {{ document().modified | customDate }}</span>
</div>
</ng-template>
@if (displayFields.includes(DisplayField.CREATED)) {
@if (displayFields().includes(DisplayField.CREATED)) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.created | customDate:'mediumDate'}}</small>
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document().created | customDate:'mediumDate'}}</small>
</div>
}
@if (displayFields.includes(DisplayField.ADDED)) {
@if (displayFields().includes(DisplayField.ADDED)) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center" [ngbTooltip]="dateTooltip">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document.added | customDate:'mediumDate'}}</small>
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="calendar-event"></i-bs><small>{{document().added | customDate:'mediumDate'}}</small>
</div>
}
}
@if (displayFields.includes(DisplayField.PAGE_COUNT) && document.page_count) {
@if (displayFields().includes(DisplayField.PAGE_COUNT) && document().page_count) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="files"></i-bs>
<small i18n>{document.page_count, plural, =1 {1 page} other {{{document.page_count}} pages}}</small>
<small i18n>{document().page_count, plural, =1 {1 page} other {{{document().page_count}} pages}}</small>
</div>
}
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
@if (displayFields().includes(DisplayField.OWNER) && document().owner && document().owner !== settingsService.currentUser().id) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document.owner | username | async}}</small>
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="person-fill-lock"></i-bs><small>{{document().owner | username | async}}</small>
</div>
}
@if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
@if (displayFields().includes(DisplayField.SHARED) && document().is_shared_by_requester) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="people-fill"></i-bs><small i18n>Shared</small>
</div>
}
@if (document.__search_hit__?.score) {
@if (document().__search_hit__?.score) {
<div class="list-group-item bg-light text-dark border-0 d-flex p-0 ps-4 search-score">
<small class="me-2 text-muted" i18n>Score:</small>
<ngb-progressbar [type]="searchScoreClass" [value]="document.__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
<ngb-progressbar [type]="searchScoreClass" [value]="document().__search_hit__.score" class="search-score-bar mx-2 mt-1" [max]="1"></ngb-progressbar>
</div>
}
@for (field of document.custom_fields; track field.field) {
@if (displayFields.includes(DisplayField.CUSTOM_FIELD + field.field)) {
@for (field of document().custom_fields; track field.field) {
@if (displayFields().includes(DisplayField.CUSTOM_FIELD + field.field)) {
<div class="list-group-item bg-light text-dark p-1 border-0 d-flex align-items-center">
<i-bs width=".9em" height=".9em" class="me-2 text-muted" name="ui-radios"></i-bs>
<small>
<pngx-custom-field-display [document]="document" [fieldId]="field.field" showNameIfEmpty="true"></pngx-custom-field-display>
<pngx-custom-field-display [document]="document()" [fieldId]="field.field" showNameIfEmpty="true"></pngx-custom-field-display>
</small>
</div>
}
@@ -2,6 +2,26 @@
overflow-wrap: anywhere;
}
.fade.show {
animation: pngx-entry-fade 160ms ease-out;
}
@keyframes pngx-entry-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.fade.show {
animation: none;
}
}
.doc-img-container {
position: relative;
}
@@ -67,16 +67,19 @@ describe('DocumentCardLargeComponent', () => {
fixture = TestBed.createComponent(DocumentCardLargeComponent)
component = fixture.componentInstance
component.document = doc
fixture.componentRef.setInput('document', {
...doc,
tags: [...doc.tags],
notes: [...doc.notes],
})
fixture.detectChanges()
jest.useFakeTimers()
})
it('should show the card', () => {
expect(component.show).toBeFalsy()
expect(component.show()).toBeTruthy()
component.ngAfterViewInit()
jest.advanceTimersByTime(100)
expect(component.show).toBeTruthy()
expect(component.show()).toBeTruthy()
})
it('should display a document', () => {
@@ -91,25 +94,34 @@ describe('DocumentCardLargeComponent', () => {
it('should display search hits with colored score', () => {
// high
component.document.__search_hit__ = {
score: 0.9,
rank: 1,
highlights: 'cheesecake',
}
fixture.componentRef.setInput('document', {
...component.document(),
__search_hit__: {
score: 0.9,
rank: 1,
highlights: 'cheesecake',
},
})
fixture.detectChanges()
let search_hit = fixture.debugElement.query(By.css('.search-score'))
expect(search_hit).not.toBeUndefined()
expect(component.searchScoreClass).toEqual('success')
// medium
component.document.__search_hit__.score = 0.6
fixture.componentRef.setInput('document', {
...component.document(),
__search_hit__: { ...component.document().__search_hit__, score: 0.6 },
})
fixture.detectChanges()
search_hit = fixture.debugElement.query(By.css('.search-score'))
expect(search_hit).not.toBeUndefined()
expect(component.searchScoreClass).toEqual('warning')
// low
component.document.__search_hit__.score = 0.1
fixture.componentRef.setInput('document', {
...component.document(),
__search_hit__: { ...component.document().__search_hit__, score: 0.1 },
})
fixture.detectChanges()
search_hit = fixture.debugElement.query(By.css('.search-score'))
expect(search_hit).not.toBeUndefined()
@@ -117,23 +129,29 @@ describe('DocumentCardLargeComponent', () => {
})
it('should display note highlights', () => {
component.document.__search_hit__ = {
score: 0.9,
rank: 1,
note_highlights: '<span>bananas</span>',
}
fixture.componentRef.setInput('document', {
...component.document(),
__search_hit__: {
score: 0.9,
rank: 1,
note_highlights: '<span>bananas</span>',
},
})
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('bananas')
expect(component.searchNoteHighlights).toContain('<span>bananas</span>')
})
it('should fall back to document content when a search hit has no highlights', () => {
component.document.__search_hit__ = {
score: 0.9,
rank: 1,
highlights: '',
note_highlights: null,
}
fixture.componentRef.setInput('document', {
...component.document(),
__search_hit__: {
score: 0.9,
rank: 1,
highlights: '',
note_highlights: null,
},
})
fixture.detectChanges()
expect(fixture.nativeElement.textContent).toContain('Cupcake ipsum')
@@ -3,10 +3,10 @@ import {
AfterViewInit,
Component,
EventEmitter,
Input,
Output,
ViewChild,
inject,
input,
} from '@angular/core'
import { RouterModule } from '@angular/router'
import {
@@ -14,7 +14,6 @@ import {
NgbTooltipModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { delay, of } from 'rxjs'
import {
DEFAULT_DISPLAY_FIELDS,
DisplayField,
@@ -65,15 +64,14 @@ export class DocumentCardLargeComponent
{
private documentService = inject(DocumentService)
settingsService = inject(SettingsService)
readonly selected = input(false)
readonly displayFields = input<string[]>(
DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
)
readonly document = input<Document>(undefined)
DisplayField = DisplayField
@Input()
selected = false
@Input()
displayFields: string[] = DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
@Output()
toggleSelected = new EventEmitter()
@@ -81,9 +79,6 @@ export class DocumentCardLargeComponent
return this.toggleSelected.observers.length > 0
}
@Input()
document: Document
@Output()
dblClickDocument = new EventEmitter()
@@ -108,18 +103,15 @@ export class DocumentCardLargeComponent
popoverHidden = true
ngAfterViewInit(): void {
of(true)
.pipe(delay(50))
.subscribe(() => {
this.show = true
})
this.show.set(true)
}
get searchScoreClass() {
if (this.document.__search_hit__) {
if (this.document.__search_hit__.score > 0.7) {
const document = this.document()
if (document.__search_hit__) {
if (document.__search_hit__.score > 0.7) {
return 'success'
} else if (this.document.__search_hit__.score > 0.3) {
} else if (document.__search_hit__.score > 0.3) {
return 'warning'
} else {
return 'danger'
@@ -129,12 +121,10 @@ export class DocumentCardLargeComponent
get searchNoteHighlights() {
let highlights = []
if (
this.document['__search_hit__'] &&
this.document['__search_hit__'].note_highlights
) {
const document = this.document()
if (document?.['__search_hit__']?.note_highlights) {
// only show notes with a match
highlights = (this.document['__search_hit__'].note_highlights as string)
highlights = (document['__search_hit__'].note_highlights as string)
.split(',')
.filter((highlight) => highlight.includes('<span'))
}
@@ -146,11 +136,11 @@ export class DocumentCardLargeComponent
}
getThumbUrl() {
return this.documentService.getThumbUrl(this.document.id)
return this.documentService.getThumbUrl(this.document().id)
}
getDownloadUrl() {
return this.documentService.getDownloadUrl(this.document.id)
return this.documentService.getDownloadUrl(this.document().id)
}
mouseLeaveCard() {
@@ -158,19 +148,21 @@ export class DocumentCardLargeComponent
}
get contentTrimmed() {
const document = this.document()
return (
this.document.content.substring(0, 500) +
(this.document.content.length > 500 ? '...' : '')
document.content.substring(0, 500) +
(document.content.length > 500 ? '...' : '')
)
}
get hasSearchHighlights() {
return Boolean(this.document?.__search_hit__?.highlights?.trim()?.length)
return Boolean(this.document()?.__search_hit__?.highlights?.trim()?.length)
}
get shouldShowContentFallback() {
const document = this.document()
return (
this.document?.__search_hit__?.score == null ||
document?.__search_hit__?.score == null ||
(!this.hasSearchHighlights && this.searchNoteHighlights.length === 0)
)
}
@@ -1,21 +1,21 @@
<div class="col p-2 h-100 fade" [class.show]="show">
<div class="card h-100 shadow-sm document-card" [class.placeholder-glow]="!document" [class.card-selected]="selected" (mouseleave)="mouseLeaveCard()">
<div class="col p-2 h-100 fade" [class.show]="show()">
<div class="card h-100 shadow-sm document-card" [class.placeholder-glow]="!document()" [class.card-selected]="selected()" (mouseleave)="mouseLeaveCard()">
<div class="border-bottom doc-img-container rounded-top" (click)="this.toggleSelected.emit($event)" (dblclick)="dblClickDocument.emit(this)">
@if (document) {
@if (document()) {
<img class="card-img doc-img" [class.inverted]="getIsThumbInverted()" [src]="getThumbUrl()">
<div class="border-end border-bottom bg-light py-1 px-2 document-card-check">
<div class="form-check">
<input type="checkbox" class="form-check-input" id="smallCardCheck{{document.id}}" [checked]="selected" (click)="this.toggleSelected.emit($event)">
<label class="form-check-label" for="smallCardCheck{{document.id}}"></label>
<input type="checkbox" class="form-check-input" id="smallCardCheck{{document().id}}" [checked]="selected()" (click)="this.toggleSelected.emit($event)">
<label class="form-check-label" for="smallCardCheck{{document().id}}"></label>
</div>
</div>
} @else {
<div class="placeholder bg-secondary w-100 card-img doc-img"></div>
}
@if (document && displayFields?.includes(DisplayField.TAGS)) {
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6" [class.tags-no-wrap]="document.tags.length > 3">
@if (document() && displayFields().includes(DisplayField.TAGS)) {
<div class="tags d-flex flex-column text-end position-absolute me-1 fs-6" [class.tags-no-wrap]="document().tags.length > 3">
@for (tagID of tagIDs; track tagID) {
<pngx-tag [tagID]="tagID" (click)="clickTag.emit(tagID);$event.stopPropagation()" [clickable]="true" linkTitle="Toggle tag filter" i18n-linkTitle></pngx-tag>
}
@@ -28,23 +28,23 @@
}
</div>
@if (document && displayFields.includes(DisplayField.NOTES) && notesEnabled && document.notes.length) {
<a routerLink="/documents/{{document.id}}/notes" class="document-card-notes py-2 px-1">
@if (document() && displayFields().includes(DisplayField.NOTES) && notesEnabled && document().notes.length) {
<a routerLink="/documents/{{document().id}}/notes" class="document-card-notes py-2 px-1">
<span class="badge rounded-pill bg-light border text-primary">
<i-bs width="1.2em" height="1.2em" class="ms-1 me-1" name="chat-left-text"></i-bs>
{{document.notes.length}}</span>
{{document().notes.length}}</span>
</a>
}
<div class="card-body bg-light p-2">
<p class="card-text">
@if (document) {
@if (displayFields.includes(DisplayField.CORRESPONDENT) && document.correspondent) {
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document.correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{document.correspondent | correspondentName | async}}</a>
@if (displayFields.includes(DisplayField.TITLE)) {:}
@if (document()) {
@if (displayFields().includes(DisplayField.CORRESPONDENT) && document().correspondent) {
<a title="Toggle correspondent filter" i18n-title (click)="clickCorrespondent.emit(document().correspondent);$event.stopPropagation()" class="fw-bold btn-link">{{document().correspondent | correspondentName | async}}</a>
@if (displayFields().includes(DisplayField.TITLE)) {:}
}
@if (displayFields.includes(DisplayField.TITLE)) {
{{document.title | documentTitle}}
@if (displayFields().includes(DisplayField.TITLE)) {
{{document().title | documentTitle}}
}
} @else {
<div class="placeholder bg-secondary w-100"></div>
@@ -54,82 +54,82 @@
</div>
<div class="card-footer pt-0 pb-2 px-2">
<div class="list-group list-group-flush border-0 pt-1 pb-2 card-info">
@if (document) {
@if (displayFields.includes(DisplayField.DOCUMENT_TYPE) && document.document_type) {
@if (document()) {
@if (displayFields().includes(DisplayField.DOCUMENT_TYPE) && document().document_type) {
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle document type filter" i18n-title
(click)="clickDocumentType.emit(document.document_type);$event.stopPropagation()">
(click)="clickDocumentType.emit(document().document_type);$event.stopPropagation()">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="file-earmark"></i-bs>
<small>{{document.document_type | documentTypeName | async}}</small>
<small>{{document().document_type | documentTypeName | async}}</small>
</button>
}
@if (displayFields.includes(DisplayField.STORAGE_PATH) && document.storage_path) {
@if (displayFields().includes(DisplayField.STORAGE_PATH) && document().storage_path) {
<button type="button" class="list-group-item list-group-item-action bg-transparent ps-0 p-1 border-0" title="Toggle storage path filter" i18n-title
(click)="clickStoragePath.emit(document.storage_path);$event.stopPropagation()">
(click)="clickStoragePath.emit(document().storage_path);$event.stopPropagation()">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="folder"></i-bs>
<small>{{document.storage_path | storagePathName | async}}</small>
<small>{{document().storage_path | storagePathName | async}}</small>
</button>
}
@if (displayFields.includes(DisplayField.CREATED)) {
@if (displayFields().includes(DisplayField.CREATED)) {
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<ng-template #dateCreatedTooltip>
<div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span>
<span i18n>Created: {{ document().created | customDate }}</span>
<span i18n>Added: {{ document().added | customDate }}</span>
<span i18n>Modified: {{ document().modified | customDate }}</span>
</div>
</ng-template>
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateCreatedTooltip">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
<small>{{document.created | customDate:'mediumDate'}}</small>
<small>{{document().created | customDate:'mediumDate'}}</small>
</div>
</div>
}
@if (displayFields.includes(DisplayField.ADDED)) {
@if (displayFields().includes(DisplayField.ADDED)) {
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<ng-template #dateAddedTooltip>
<div class="d-flex flex-column text-light">
<span i18n>Created: {{ document.created | customDate }}</span>
<span i18n>Added: {{ document.added | customDate }}</span>
<span i18n>Modified: {{ document.modified | customDate }}</span>
<span i18n>Created: {{ document().created | customDate }}</span>
<span i18n>Added: {{ document().added | customDate }}</span>
<span i18n>Modified: {{ document().modified | customDate }}</span>
</div>
</ng-template>
<div class="ps-0 p-1" placement="top" [ngbTooltip]="dateAddedTooltip">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="calendar-event"></i-bs>
<small>{{document.added | customDate:'mediumDate'}}</small>
<small>{{document().added | customDate:'mediumDate'}}</small>
</div>
</div>
}
@if (displayFields.includes(DisplayField.PAGE_COUNT) && document.page_count) {
@if (displayFields().includes(DisplayField.PAGE_COUNT) && document().page_count) {
<div class="list-group-item bg-transparent p-0 border-0 d-flex flex-wrap-reverse justify-content-between">
<div class="ps-0 p-1" placement="top">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="files"></i-bs>
<small i18n>{document.page_count, plural, =1 {1 page} other {{{document.page_count}} pages}}</small>
<small i18n>{document().page_count, plural, =1 {1 page} other {{{document().page_count}} pages}}</small>
</div>
</div>
}
@if (displayFields.includes(DisplayField.ASN) && document.archive_serial_number | isNumber) {
@if (displayFields().includes(DisplayField.ASN) && document().archive_serial_number | isNumber) {
<div class="ps-0 p-1">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="upc-scan"></i-bs>
<small>#{{document.archive_serial_number}}</small>
<small>#{{document().archive_serial_number}}</small>
</div>
}
@if (displayFields.includes(DisplayField.OWNER) && document.owner && document.owner !== settingsService.currentUser.id) {
@if (displayFields().includes(DisplayField.OWNER) && document().owner && document().owner !== settingsService.currentUser().id) {
<div class="ps-0 p-1">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="person-fill-lock"></i-bs>
<small>{{document.owner | username | async}}</small>
<small>{{document().owner | username | async}}</small>
</div>
}
@if (displayFields.includes(DisplayField.SHARED) && document.is_shared_by_requester) {
@if (displayFields().includes(DisplayField.SHARED) && document().is_shared_by_requester) {
<div class="ps-0 p-1">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="people-fill"></i-bs>
<small i18n>Shared</small>
</div>
}
@for (field of document.custom_fields; track field.field) {
@if (displayFields.includes(DisplayField.CUSTOM_FIELD + field.field)) {
@for (field of document().custom_fields; track field.field) {
@if (displayFields().includes(DisplayField.CUSTOM_FIELD + field.field)) {
<div class="ps-0 p-1 d-flex align-items-center overflow-hidden">
<i-bs width="1em" height="1em" class="me-2 text-muted" name="ui-radios"></i-bs>
<small><pngx-custom-field-display [document]="document" [fieldId]="field.field" showNameIfEmpty="true"></pngx-custom-field-display></small>
<small><pngx-custom-field-display [document]="document()" [fieldId]="field.field" showNameIfEmpty="true"></pngx-custom-field-display></small>
</div>
}
}
@@ -139,11 +139,11 @@
</div>
<div class="d-flex justify-content-between align-items-center">
<div class="btn-group w-100">
@if (document) {
<a routerLink="/documents/{{document.id}}" class="btn btn-sm btn-outline-secondary" title="Open" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
@if (document()) {
<a routerLink="/documents/{{document().id}}" class="btn btn-sm btn-outline-secondary" title="Open" i18n-title *pngxIfPermissions="{ action: PermissionAction.Change, type: PermissionType.Document }" i18n-title>
<i-bs name="file-earmark-richtext"></i-bs>
</a>
<pngx-preview-popup [document]="document" #popupPreview>
<pngx-preview-popup [document]="document()" #popupPreview>
<i-bs name="eye"></i-bs>
</pngx-preview-popup>
<a [href]="getDownloadUrl()" class="btn btn-sm btn-outline-secondary" title="Download" i18n-title (click)="$event.stopPropagation()">
@@ -2,6 +2,26 @@
font-size: 90%;
}
.fade.show {
animation: pngx-entry-fade 160ms ease-out;
}
@keyframes pngx-entry-fade {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@media (prefers-reduced-motion: reduce) {
.fade.show {
animation: none;
}
}
.doc-img {
object-fit: cover;
object-position: top left;
@@ -46,16 +46,15 @@ describe('DocumentCardSmallComponent', () => {
fixture = TestBed.createComponent(DocumentCardSmallComponent)
component = fixture.componentInstance
component.document = Object.assign({}, doc)
fixture.componentRef.setInput('document', Object.assign({}, doc))
fixture.detectChanges()
jest.useFakeTimers()
})
it('should show the card', () => {
expect(component.show).toBeFalsy()
expect(component.show()).toBeTruthy()
component.ngAfterViewInit()
jest.advanceTimersByTime(100)
expect(component.show).toBeTruthy()
expect(component.show()).toBeTruthy()
})
it('should display page count', () => {
@@ -67,7 +66,10 @@ describe('DocumentCardSmallComponent', () => {
expect(
fixture.debugElement.queryAll(By.directive(TagComponent))
).toHaveLength(5)
component.document.tags = [1, 2]
fixture.componentRef.setInput('document', {
...component.document(),
tags: [1, 2],
})
fixture.detectChanges()
expect(
fixture.debugElement.queryAll(By.directive(TagComponent))
@@ -75,7 +77,10 @@ describe('DocumentCardSmallComponent', () => {
})
it('should increase limit tags to 6 if no notes', () => {
component.document.notes = []
fixture.componentRef.setInput('document', {
...component.document(),
notes: [],
})
fixture.detectChanges()
expect(
fixture.debugElement.queryAll(By.directive(TagComponent))
@@ -85,7 +90,10 @@ describe('DocumentCardSmallComponent', () => {
it('should clear hidden tag counter when tag count falls below the limit', () => {
expect(component.moreTags).toEqual(3)
component.document.tags = [1, 2, 3, 4, 5, 6]
fixture.componentRef.setInput('document', {
...component.document(),
tags: [1, 2, 3, 4, 5, 6],
})
fixture.detectChanges()
expect(component.moreTags).toBeNull()
@@ -3,10 +3,10 @@ import {
AfterViewInit,
Component,
EventEmitter,
Input,
Output,
ViewChild,
inject,
input,
} from '@angular/core'
import { RouterModule } from '@angular/router'
import {
@@ -14,8 +14,6 @@ import {
NgbTooltipModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import { delay } from 'rxjs/operators'
import {
DEFAULT_DISPLAY_FIELDS,
DisplayField,
@@ -66,21 +64,17 @@ export class DocumentCardSmallComponent
{
private documentService = inject(DocumentService)
settingsService = inject(SettingsService)
readonly selected = input(false)
readonly document = input<Document>(undefined)
readonly displayFields = input<string[]>(
DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
)
DisplayField = DisplayField
@Input()
selected = false
@Output()
toggleSelected = new EventEmitter()
@Input()
document: Document
@Input()
displayFields: string[] = DEFAULT_DISPLAY_FIELDS.map((f) => f.id)
@Output()
dblClickDocument = new EventEmitter()
@@ -96,16 +90,18 @@ export class DocumentCardSmallComponent
@Output()
clickStoragePath = new EventEmitter<number>()
moreTags: number = null
get moreTags(): number {
const document = this.document()
const limit = document?.notes.length > 0 ? 6 : 7
return document?.tags.length > limit
? document.tags.length - (limit - 1)
: null
}
@ViewChild('popupPreview') popupPreview: PreviewPopupComponent
ngAfterViewInit(): void {
of(true)
.pipe(delay(50))
.subscribe(() => {
this.show = true
})
this.show.set(true)
}
getIsThumbInverted() {
@@ -113,21 +109,20 @@ export class DocumentCardSmallComponent
}
getThumbUrl() {
return this.documentService.getThumbUrl(this.document.id)
return this.documentService.getThumbUrl(this.document().id)
}
getDownloadUrl() {
return this.documentService.getDownloadUrl(this.document.id)
return this.documentService.getDownloadUrl(this.document().id)
}
get tagIDs() {
const limit = this.document.notes.length > 0 ? 6 : 7
if (this.document.tags.length > limit) {
this.moreTags = this.document.tags.length - (limit - 1)
return this.document.tags.slice(0, limit - 1)
const document = this.document()
const limit = document.notes.length > 0 ? 6 : 7
if (document.tags.length > limit) {
return document.tags.slice(0, limit - 1)
} else {
this.moreTags = null
return this.document.tags
return document.tags
}
}
@@ -36,7 +36,7 @@
</button>
<div ngbDropdownMenu aria-labelledby="dropdownDisplayFields" class="shadow">
<div class="px-3">
@for (field of settingsService.allDisplayFields; track field.id) {
@for (field of settingsService.allDisplayFields(); track field.id) {
<div class="form-check my-1">
<input class="form-check-input mt-1" type="checkbox" id="displayField{{field.id}}" [checked]="activeDisplayFields.includes(field.id)" (change)="toggleDisplayField(field.id)">
<label class="form-check-label" for="displayField{{field.id}}">{{field.name}}</label>
@@ -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', () => {
@@ -777,7 +779,7 @@ describe('DocumentListComponent', () => {
})
it('should hide columns if no perms or notes disabled', () => {
jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(true)
permissionService.initialize([], { is_superuser: true } as any)
jest.spyOn(documentListService, 'documents', 'get').mockReturnValue(docs)
expect(documentListService.sortField).toEqual('created')
@@ -798,7 +800,7 @@ describe('DocumentListComponent', () => {
).toHaveLength(9)
// insufficient perms
jest.spyOn(permissionService, 'currentUserCan').mockReturnValue(false)
permissionService.initialize([], { is_superuser: false } as any)
fixture.detectChanges()
expect(
fixture.debugElement.queryAll(By.directive(SortableDirective))
@@ -837,11 +839,10 @@ describe('DocumentListComponent', () => {
it('should get custom field title', () => {
fixture.detectChanges()
jest
.spyOn(settingsService, 'allDisplayFields', 'get')
.mockReturnValue([
{ id: 'custom_field_1' as any, name: 'Custom Field 1' },
])
const mockDisplayFields = [
{ id: 'custom_field_1' as any, name: 'Custom Field 1' },
]
settingsService.allDisplayFields = jest.fn(() => mockDisplayFields) as any
expect(component.getDisplayCustomFieldTitle('custom_field_1')).toEqual(
'Custom Field 1'
)
@@ -255,7 +255,7 @@ export class DocumentListComponent
}
public getDisplayCustomFieldTitle(field: string) {
return this.settingsService.allDisplayFields.find((f) => f.id === field)
return this.settingsService.allDisplayFields().find((f) => f.id === field)
?.name
}
@@ -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)
},
})
})
@@ -570,6 +572,7 @@ export class DocumentListComponent
}
get notesEnabled(): boolean {
this.settingsService.trackChanges()
return this.settingsService.get(SETTINGS_KEYS.NOTES_ENABLED)
}
@@ -1,6 +1,6 @@
<div class="row flex-wrap row-gap-3" tourAnchor="tour.documents-filter-editor">
<div class="col">
<div class="form-inline d-flex align-items-center fade" [class.show]="show">
<div class="form-inline d-flex align-items-center fade" [class.show]="show()">
<div class="input-group input-group-sm flex-fill w-auto flex-nowrap">
<div ngbDropdown>
<button class="btn btn-sm btn-outline-primary" ngbDropdownToggle>{{textFilterTargetName}}</button>
@@ -36,7 +36,7 @@
<div class="d-flex flex-wrap gap-3">
<div class="d-flex flex-wrap gap-2">
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Tag) && tagSelectionModel.items.length > 0) {
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Tags" icon="tag-fill" i18n-title
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show()" title="Tags" icon="tag-fill" i18n-title
filterPlaceholder="Filter tags" i18n-filterPlaceholder
[(selectionModel)]="tagSelectionModel"
(selectionModelChange)="updateRules()"
@@ -47,7 +47,7 @@
shortcutKey="t"></pngx-filterable-dropdown>
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.Correspondent) && correspondentSelectionModel.items.length > 0) {
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Correspondent" icon="person-fill" i18n-title
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show()" title="Correspondent" icon="person-fill" i18n-title
filterPlaceholder="Filter correspondents" i18n-filterPlaceholder
[(selectionModel)]="correspondentSelectionModel"
(selectionModelChange)="updateRules()"
@@ -58,7 +58,7 @@
shortcutKey="y"></pngx-filterable-dropdown>
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.DocumentType) && documentTypeSelectionModel.items.length > 0) {
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Document type" icon="file-earmark-fill" i18n-title
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show()" title="Document type" icon="file-earmark-fill" i18n-title
filterPlaceholder="Filter document types" i18n-filterPlaceholder
[(selectionModel)]="documentTypeSelectionModel"
(selectionModelChange)="updateRules()"
@@ -69,7 +69,7 @@
shortcutKey="u"></pngx-filterable-dropdown>
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.StoragePath) && storagePathSelectionModel.items.length > 0) {
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show" title="Storage path" icon="folder-fill" i18n-title
<pngx-filterable-dropdown class="flex-fill fade" [class.show]="show()" title="Storage path" icon="folder-fill" i18n-title
filterPlaceholder="Filter storage paths" i18n-filterPlaceholder
[(selectionModel)]="storagePathSelectionModel"
(selectionModelChange)="updateRules()"
@@ -81,12 +81,12 @@
}
@if (permissionsService.currentUserCan(PermissionAction.View, PermissionType.CustomField) && customFields.length > 0) {
<pngx-custom-fields-query-dropdown class="flex-fill fade" [class.show]="show" title="Custom fields" icon="ui-radios" i18n-title
<pngx-custom-fields-query-dropdown class="flex-fill fade" [class.show]="show()" title="Custom fields" icon="ui-radios" i18n-title
[(selectionModel)]="customFieldQueriesModel"
(selectionModelChange)="updateRules()"
></pngx-custom-fields-query-dropdown>
}
<pngx-dates-dropdown class="flex-fill fade" [class.show]="show"
<pngx-dates-dropdown class="flex-fill fade" [class.show]="show()"
title="Dates" i18n-title
placement="bottom-end"
(datesSet)="updateRules()"
@@ -97,7 +97,7 @@
[(addedDateFrom)]="dateAddedFrom"
[(addedRelativeDate)]="dateAddedRelativeDate">
</pngx-dates-dropdown>
<pngx-permissions-filter-dropdown class="flex-fill fade" [class.show]="show"
<pngx-permissions-filter-dropdown class="flex-fill fade" [class.show]="show()"
title="Permissions" i18n-title
(ownerFilterSet)="updateRules()"
[(selectionModel)]="permissionsSelectionModel"></pngx-permissions-filter-dropdown>
@@ -4,12 +4,7 @@ import {
HttpTestingController,
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 { RouterModule } from '@angular/router'
@@ -189,8 +184,13 @@ describe('FilterEditorComponent', () => {
let httpTestingController: HttpTestingController
let searchService: SearchService
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
const tick = (ms: number = 0) => {
jest.advanceTimersByTime(ms)
fixture.detectChanges()
}
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
RouterModule,
NgbDropdownModule,
@@ -260,7 +260,7 @@ describe('FilterEditorComponent', () => {
documentService = TestBed.inject(DocumentService)
settingsService = TestBed.inject(SettingsService)
settingsService.currentUser = users[0]
settingsService.currentUser.set(users[0])
permissionsService = TestBed.inject(PermissionsService)
searchService = TestBed.inject(SearchService)
jest
@@ -275,8 +275,13 @@ describe('FilterEditorComponent', () => {
component = fixture.componentInstance
component.filterRules = []
fixture.detectChanges()
tick()
}))
await fixture.whenStable()
jest.useFakeTimers()
})
afterEach(() => {
jest.useRealTimers()
})
it('should not attempt to retrieve objects if user does not have permissions', () => {
jest.spyOn(permissionsService, 'currentUserCan').mockReset()
@@ -298,7 +303,7 @@ describe('FilterEditorComponent', () => {
// SET filterRules
it('should ingest text filter rules for doc title', fakeAsync(() => {
it('should ingest text filter rules for doc title', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -308,9 +313,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('foo')
expect(component.textFilterTarget).toEqual('title') // TEXT_FILTER_TARGET_TITLE
}))
})
it('should ingest text filter rules for doc title + content', fakeAsync(() => {
it('should ingest text filter rules for doc title + content', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -320,9 +325,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('foo')
expect(component.textFilterTarget).toEqual('title-content') // TEXT_FILTER_TARGET_TITLE_CONTENT
}))
})
it('should ingest legacy text filter rules for doc title + content', fakeAsync(() => {
it('should ingest legacy text filter rules for doc title + content', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -332,9 +337,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('legacy foo')
expect(component.textFilterTarget).toEqual('title-content') // TEXT_FILTER_TARGET_TITLE_CONTENT
}))
})
it('should ingest text filter rules for doc asn', fakeAsync(() => {
it('should ingest text filter rules for doc asn', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -344,9 +349,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('foo')
expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
}))
})
it('should ingest text filter rules for custom fields', fakeAsync(() => {
it('should ingest text filter rules for custom fields', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -356,9 +361,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('foo')
expect(component.textFilterTarget).toEqual('custom-fields') // TEXT_FILTER_TARGET_CUSTOM_FIELDS
}))
})
it('should ingest text filter rules for doc asn is null', fakeAsync(() => {
it('should ingest text filter rules for doc asn is null', () => {
expect(component.textFilterTarget).toEqual('title-content')
expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
component.filterRules = [
@@ -369,9 +374,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
expect(component.textFilterModifier).toEqual('is null') // TEXT_FILTER_MODIFIER_NULL
}))
})
it('should ingest text filter rules for doc asn is not null', fakeAsync(() => {
it('should ingest text filter rules for doc asn is not null', () => {
expect(component.textFilterTarget).toEqual('title-content')
expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
component.filterRules = [
@@ -382,9 +387,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
expect(component.textFilterModifier).toEqual('not null') // TEXT_FILTER_MODIFIER_NOTNULL
}))
})
it('should ingest text filter rules for doc asn greater than', fakeAsync(() => {
it('should ingest text filter rules for doc asn greater than', () => {
expect(component.textFilterTarget).toEqual('title-content')
expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
component.filterRules = [
@@ -395,9 +400,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
expect(component.textFilterModifier).toEqual('greater') // TEXT_FILTER_MODIFIER_GT
}))
})
it('should ingest text filter rules for doc asn less than', fakeAsync(() => {
it('should ingest text filter rules for doc asn less than', () => {
expect(component.textFilterTarget).toEqual('title-content')
expect(component.textFilterModifier).toEqual('equals') // TEXT_FILTER_MODIFIER_EQUALS
component.filterRules = [
@@ -408,9 +413,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilterTarget).toEqual('asn') // TEXT_FILTER_TARGET_ASN
expect(component.textFilterModifier).toEqual('less') // TEXT_FILTER_MODIFIER_LT
}))
})
it('should ingest text filter rules for mime type', fakeAsync(() => {
it('should ingest text filter rules for mime type', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -420,9 +425,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('pdf')
expect(component.textFilterTarget).toEqual('mime-type') // TEXT_FILTER_TARGET_MIME_TYPE
}))
})
it('should ingest text filter rules for fulltext query', fakeAsync(() => {
it('should ingest text filter rules for fulltext query', () => {
expect(component.textFilter).toEqual(null)
component.filterRules = [
{
@@ -432,9 +437,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.textFilter).toEqual('foo,bar')
expect(component.textFilterTarget).toEqual('fulltext-query') // TEXT_FILTER_TARGET_FULLTEXT_QUERY
}))
})
it('should ingest text filter rules for fulltext query that include date created', fakeAsync(() => {
it('should ingest text filter rules for fulltext query that include date created', () => {
expect(component.dateCreatedRelativeDate).toBeNull()
component.filterRules = [
{
@@ -444,9 +449,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.dateCreatedRelativeDate).toEqual(1) // RELATIVE_DATE_QUERYSTRINGS['-1 week to now']
expect(component.textFilter).toBeNull()
}))
})
it('should ingest text filter rules for fulltext query that include date added', fakeAsync(() => {
it('should ingest text filter rules for fulltext query that include date added', () => {
expect(component.dateAddedRelativeDate).toBeNull()
component.filterRules = [
{
@@ -456,9 +461,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.dateAddedRelativeDate).toEqual(1) // RELATIVE_DATE_QUERYSTRINGS['-1 week to now']
expect(component.textFilter).toBeNull()
}))
})
it('should ingest text filter content with relative dates that are not in quick list', fakeAsync(() => {
it('should ingest text filter content with relative dates that are not in quick list', () => {
expect(component.dateAddedRelativeDate).toBeNull()
component.filterRules = [
{
@@ -478,9 +483,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.dateCreatedRelativeDate).toBeNull()
expect(component.textFilter).toEqual('created:[-2 week to now]')
}))
})
it('should ingest text filter rules for more like', fakeAsync(() => {
it('should ingest text filter rules for more like', () => {
const moreLikeSpy = jest.spyOn(documentService, 'get')
moreLikeSpy.mockReturnValue(of({ id: 1, title: 'Foo Bar' }))
expect(component.textFilter).toEqual(null)
@@ -500,9 +505,9 @@ describe('FilterEditorComponent', () => {
value: '1',
},
])
}))
})
it('should ingest filter rules for date created after and adjust date by 1 day', fakeAsync(() => {
it('should ingest filter rules for date created after and adjust date by 1 day', () => {
expect(component.dateCreatedFrom).toBeNull()
component.filterRules = [
{
@@ -511,9 +516,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateCreatedFrom).toEqual('2023-05-15')
}))
})
it('should ingest filter rules for date created from', fakeAsync(() => {
it('should ingest filter rules for date created from', () => {
expect(component.dateCreatedFrom).toBeNull()
component.filterRules = [
{
@@ -522,9 +527,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateCreatedFrom).toEqual('2023-05-14')
}))
})
it('should ingest filter rules for date created before and adjust date by 1 day', fakeAsync(() => {
it('should ingest filter rules for date created before and adjust date by 1 day', () => {
expect(component.dateCreatedTo).toBeNull()
component.filterRules = [
{
@@ -533,9 +538,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateCreatedTo).toEqual('2023-05-13')
}))
})
it('should ingest filter rules for date created to', fakeAsync(() => {
it('should ingest filter rules for date created to', () => {
expect(component.dateCreatedTo).toBeNull()
component.filterRules = [
{
@@ -544,9 +549,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateCreatedTo).toEqual('2023-05-14')
}))
})
it('should ingest filter rules for date added after and adjust date by 1 day', fakeAsync(() => {
it('should ingest filter rules for date added after and adjust date by 1 day', () => {
expect(component.dateAddedFrom).toBeNull()
component.filterRules = [
{
@@ -555,9 +560,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateAddedFrom).toEqual('2023-05-15')
}))
})
it('should ingest filter rules for date added from', fakeAsync(() => {
it('should ingest filter rules for date added from', () => {
expect(component.dateAddedFrom).toBeNull()
component.filterRules = [
{
@@ -566,9 +571,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateAddedFrom).toEqual('2023-05-14')
}))
})
it('should ingest filter rules for date added before and adjust date by 1 day', fakeAsync(() => {
it('should ingest filter rules for date added before and adjust date by 1 day', () => {
expect(component.dateAddedTo).toBeNull()
component.filterRules = [
{
@@ -577,9 +582,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateAddedTo).toEqual('2023-05-13')
}))
})
it('should ingest filter rules for date added to', fakeAsync(() => {
it('should ingest filter rules for date added to', () => {
expect(component.dateAddedTo).toBeNull()
component.filterRules = [
{
@@ -588,9 +593,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.dateAddedTo).toEqual('2023-05-14')
}))
})
it('should ingest filter rules for has all tags', fakeAsync(() => {
it('should ingest filter rules for has all tags', () => {
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
component.filterRules = [
{
@@ -614,9 +619,9 @@ describe('FilterEditorComponent', () => {
},
]
component.toggleTag(2) // coverage
}))
})
it('should ingest filter rules for has any tags', fakeAsync(() => {
it('should ingest filter rules for has any tags', () => {
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
component.filterRules = [
{
@@ -639,9 +644,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for has any tag', fakeAsync(() => {
it('should ingest filter rules for has any tag', () => {
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(0)
component.filterRules = [
{
@@ -651,9 +656,9 @@ describe('FilterEditorComponent', () => {
]
expect(component.tagSelectionModel.getSelectedItems()).toHaveLength(1)
expect(component.tagSelectionModel.get(null)).toBeTruthy()
}))
})
it('should ingest filter rules for exclude tag(s)', fakeAsync(() => {
it('should ingest filter rules for exclude tag(s)', () => {
expect(component.tagSelectionModel.getExcludedItems()).toHaveLength(0)
component.filterRules = [
{
@@ -676,9 +681,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for has correspondent', fakeAsync(() => {
it('should ingest filter rules for has correspondent', () => {
expect(
component.correspondentSelectionModel.getSelectedItems()
).toHaveLength(0)
@@ -708,9 +713,9 @@ describe('FilterEditorComponent', () => {
expect(component.correspondentSelectionModel.getExcludedItems()).toEqual([
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
])
}))
})
it('should ingest filter rules for has any of correspondents', fakeAsync(() => {
it('should ingest filter rules for has any of correspondents', () => {
expect(
component.correspondentSelectionModel.getSelectedItems()
).toHaveLength(0)
@@ -740,9 +745,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for does not have any of correspondents', fakeAsync(() => {
it('should ingest filter rules for does not have any of correspondents', () => {
expect(
component.correspondentSelectionModel.getExcludedItems()
).toHaveLength(0)
@@ -769,9 +774,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for has document type', fakeAsync(() => {
it('should ingest filter rules for has document type', () => {
expect(
component.documentTypeSelectionModel.getSelectedItems()
).toHaveLength(0)
@@ -801,9 +806,9 @@ describe('FilterEditorComponent', () => {
expect(component.documentTypeSelectionModel.getExcludedItems()).toEqual([
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
])
}))
})
it('should ingest filter rules for has any of document types', fakeAsync(() => {
it('should ingest filter rules for has any of document types', () => {
expect(
component.documentTypeSelectionModel.getSelectedItems()
).toHaveLength(0)
@@ -830,9 +835,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for does not have any of document types', fakeAsync(() => {
it('should ingest filter rules for does not have any of document types', () => {
expect(
component.documentTypeSelectionModel.getExcludedItems()
).toHaveLength(0)
@@ -859,9 +864,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for has storage path', fakeAsync(() => {
it('should ingest filter rules for has storage path', () => {
expect(component.storagePathSelectionModel.getSelectedItems()).toHaveLength(
0
)
@@ -891,9 +896,9 @@ describe('FilterEditorComponent', () => {
expect(component.storagePathSelectionModel.getExcludedItems()).toEqual([
{ id: NEGATIVE_NULL_FILTER_VALUE, name: 'Not assigned' },
])
}))
})
it('should ingest filter rules for has any of storage paths', fakeAsync(() => {
it('should ingest filter rules for has any of storage paths', () => {
expect(component.storagePathSelectionModel.getSelectedItems()).toHaveLength(
0
)
@@ -923,9 +928,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for does not have any of storage paths', fakeAsync(() => {
it('should ingest filter rules for does not have any of storage paths', () => {
expect(component.storagePathSelectionModel.getExcludedItems()).toHaveLength(
0
)
@@ -952,9 +957,9 @@ describe('FilterEditorComponent', () => {
value: null,
},
]
}))
})
it('should ingest filter rules for custom fields all', fakeAsync(() => {
it('should ingest filter rules for custom fields all', () => {
expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy()
component.filterRules = [
{
@@ -972,9 +977,9 @@ describe('FilterEditorComponent', () => {
.value[0] as CustomFieldQueryAtom
).serialize()
).toEqual(['42', CustomFieldQueryOperator.Exists, 'true'])
}))
})
it('should ingest filter rules for has any custom fields', fakeAsync(() => {
it('should ingest filter rules for has any custom fields', () => {
expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy()
component.filterRules = [
{
@@ -992,9 +997,9 @@ describe('FilterEditorComponent', () => {
.value[0] as CustomFieldQueryAtom
).serialize()
).toEqual(['42', CustomFieldQueryOperator.Exists, 'true'])
}))
})
it('should ingest filter rules for custom field queries', fakeAsync(() => {
it('should ingest filter rules for custom field queries', () => {
expect(component.customFieldQueriesModel.isEmpty()).toBeTruthy()
component.filterRules = [
{
@@ -1027,9 +1032,9 @@ describe('FilterEditorComponent', () => {
.value[0] as CustomFieldQueryAtom
).serialize()
).toEqual([42, CustomFieldQueryOperator.Exists, 'true'])
}))
})
it('should ingest filter rules for owner', fakeAsync(() => {
it('should ingest filter rules for owner', () => {
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
OwnerFilterType.NONE
)
@@ -1044,9 +1049,9 @@ describe('FilterEditorComponent', () => {
)
expect(component.permissionsSelectionModel.hideUnowned).toBeFalsy()
expect(component.permissionsSelectionModel.userID).toEqual(100)
}))
})
it('should ingest filter rules for owner is others', fakeAsync(() => {
it('should ingest filter rules for owner is others', () => {
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
OwnerFilterType.NONE
)
@@ -1060,9 +1065,9 @@ describe('FilterEditorComponent', () => {
OwnerFilterType.OTHERS
)
expect(component.permissionsSelectionModel.includeUsers).toContain(50)
}))
})
it('should ingest filter rules for owner does not include others', fakeAsync(() => {
it('should ingest filter rules for owner does not include others', () => {
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
OwnerFilterType.NONE
)
@@ -1076,9 +1081,9 @@ describe('FilterEditorComponent', () => {
OwnerFilterType.NOT_SELF
)
expect(component.permissionsSelectionModel.excludeUsers).toContain(50)
}))
})
it('should ingest filter rules for owner is null', fakeAsync(() => {
it('should ingest filter rules for owner is null', () => {
expect(component.permissionsSelectionModel.ownerFilter).toEqual(
OwnerFilterType.NONE
)
@@ -1092,9 +1097,9 @@ describe('FilterEditorComponent', () => {
OwnerFilterType.UNOWNED
)
expect(component.permissionsSelectionModel.hideUnowned).toBeFalsy()
}))
})
it('should ingest filter rules for owner is not null', fakeAsync(() => {
it('should ingest filter rules for owner is not null', () => {
component.filterRules = [
{
rule_type: FILTER_OWNER_ISNULL,
@@ -1109,9 +1114,9 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.permissionsSelectionModel.hideUnowned).toBeTruthy()
}))
})
it('should ingest filter rules for shared by me', fakeAsync(() => {
it('should ingest filter rules for shared by me', () => {
component.filterRules = [
{
rule_type: FILTER_SHARED_BY_USER,
@@ -1119,11 +1124,11 @@ describe('FilterEditorComponent', () => {
},
]
expect(component.permissionsSelectionModel.userID).toEqual(2)
}))
})
// GET filterRules
it('should convert user input to correct filter rules on text field search title + content', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search title + content', () => {
component.textFilterInput.nativeElement.value = 'foo'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
fixture.detectChanges()
@@ -1135,9 +1140,9 @@ describe('FilterEditorComponent', () => {
value: 'foo',
},
])
}))
})
it('should convert user input to correct filter rules on text field search title only', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search title only', () => {
component.textFilterInput.nativeElement.value = 'foo'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.query(
@@ -1154,9 +1159,9 @@ describe('FilterEditorComponent', () => {
value: 'foo',
},
])
}))
})
it('should convert user input to correct filter rules on text field search equals asn', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search equals asn', () => {
component.textFilterInput.nativeElement.value = '1234'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1174,9 +1179,9 @@ describe('FilterEditorComponent', () => {
value: '1234',
},
])
}))
})
it('should convert user input to correct filter rules on text field search greater than asn', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search greater than asn', () => {
component.textFilterInput.nativeElement.value = '123'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1198,9 +1203,9 @@ describe('FilterEditorComponent', () => {
value: '123',
},
])
}))
})
it('should convert user input to correct filter rules on text field search less than asn', fakeAsync(() => {
it('should convert user input to correct filter rules on text field search less than asn', () => {
component.textFilterInput.nativeElement.value = '999'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1222,9 +1227,9 @@ describe('FilterEditorComponent', () => {
value: '999',
},
])
}))
})
it('should convert user input to correct filter rules on asn is null', fakeAsync(() => {
it('should convert user input to correct filter rules on asn is null', () => {
const textFieldTargetDropdown = fixture.debugElement.queryAll(
By.directive(NgbDropdownItem)
)[2]
@@ -1242,9 +1247,9 @@ describe('FilterEditorComponent', () => {
value: 'true',
},
])
}))
})
it('should convert user input to correct filter rules on asn is not null', fakeAsync(() => {
it('should convert user input to correct filter rules on asn is not null', () => {
const textFieldTargetDropdown = fixture.debugElement.queryAll(
By.directive(NgbDropdownItem)
)[2]
@@ -1262,9 +1267,9 @@ describe('FilterEditorComponent', () => {
value: 'false',
},
])
}))
})
it('should convert user input to correct filter rules on mime type', fakeAsync(() => {
it('should convert user input to correct filter rules on mime type', () => {
component.textFilterInput.nativeElement.value = 'pdf'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1280,9 +1285,9 @@ describe('FilterEditorComponent', () => {
value: 'pdf',
},
])
}))
})
it('should convert user input to correct filter rules on full text query', fakeAsync(() => {
it('should convert user input to correct filter rules on full text query', () => {
component.textFilterInput.nativeElement.value = 'foo'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1298,9 +1303,9 @@ describe('FilterEditorComponent', () => {
value: 'foo',
},
])
}))
})
it('should convert user input to correct filter rules on tag select not assigned', fakeAsync(() => {
it('should convert user input to correct filter rules on tag select not assigned', () => {
const tagsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[0]
@@ -1317,9 +1322,9 @@ describe('FilterEditorComponent', () => {
value: 'false',
},
])
}))
})
it('should convert user input to correct filter rules on tag selections', fakeAsync(() => {
it('should convert user input to correct filter rules on tag selections', () => {
const tagsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[0] // Tags dropdown
@@ -1369,9 +1374,9 @@ describe('FilterEditorComponent', () => {
value: tags[1].id.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on correspondent selections', fakeAsync(() => {
it('should convert user input to correct filter rules on correspondent selections', () => {
const correspondentsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[1] // Corresp dropdown
@@ -1409,9 +1414,9 @@ describe('FilterEditorComponent', () => {
value: correspondents[1].id.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on correspondent select not assigned', fakeAsync(() => {
it('should convert user input to correct filter rules on correspondent select not assigned', () => {
const correspondentsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[1]
@@ -1441,9 +1446,9 @@ describe('FilterEditorComponent', () => {
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on document type selections', fakeAsync(() => {
it('should convert user input to correct filter rules on document type selections', () => {
const documentTypesFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[2] // DocType dropdown
@@ -1481,9 +1486,9 @@ describe('FilterEditorComponent', () => {
value: document_types[1].id.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on doc type select not assigned', fakeAsync(() => {
it('should convert user input to correct filter rules on doc type select not assigned', () => {
const docTypesFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[2]
@@ -1513,9 +1518,9 @@ describe('FilterEditorComponent', () => {
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on storage path selections', fakeAsync(() => {
it('should convert user input to correct filter rules on storage path selections', () => {
const storagePathFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[3] // StoragePath dropdown
@@ -1553,9 +1558,9 @@ describe('FilterEditorComponent', () => {
value: storage_paths[1].id.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on storage path select not assigned', fakeAsync(() => {
it('should convert user input to correct filter rules on storage path select not assigned', () => {
const storagePathsFilterableDropdown = fixture.debugElement.queryAll(
By.directive(FilterableDropdownComponent)
)[3]
@@ -1585,9 +1590,9 @@ describe('FilterEditorComponent', () => {
value: NEGATIVE_NULL_FILTER_VALUE.toString(),
},
])
}))
})
it('should convert user input to correct filter rules on custom field selections', fakeAsync(() => {
it('should convert user input to correct filter rules on custom field selections', () => {
const customFieldsQueryDropdown = fixture.debugElement.queryAll(
By.directive(CustomFieldsQueryDropdownComponent)
)[0]
@@ -1617,9 +1622,9 @@ describe('FilterEditorComponent', () => {
]),
},
])
}))
})
it('should convert user input to correct filter rules on date created from', fakeAsync(() => {
it('should convert user input to correct filter rules on date created from', () => {
const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DatesDropdownComponent)
)[0]
@@ -1637,9 +1642,9 @@ describe('FilterEditorComponent', () => {
value: '2023-05-14',
},
])
}))
})
it('should convert user input to correct filter rules on date created to', fakeAsync(() => {
it('should convert user input to correct filter rules on date created to', () => {
const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DatesDropdownComponent)
)[0]
@@ -1657,9 +1662,9 @@ describe('FilterEditorComponent', () => {
value: '2023-05-14',
},
])
}))
})
it('should convert user input to correct filter rules on date created with relative date', fakeAsync(() => {
it('should convert user input to correct filter rules on date created with relative date', () => {
const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DatesDropdownComponent)
)[0]
@@ -1673,9 +1678,9 @@ describe('FilterEditorComponent', () => {
value: 'created:[-1 week to now]',
},
])
}))
})
it('should carry over text filtering on date created with relative date', fakeAsync(() => {
it('should carry over text filtering on date created with relative date', () => {
component.textFilter = 'foo'
const dateCreatedDropdown = fixture.debugElement.queryAll(
By.directive(DatesDropdownComponent)
@@ -1690,9 +1695,9 @@ describe('FilterEditorComponent', () => {
value: 'foo,created:[-1 week to now]',
},
])
}))
})
it('should convert legacy title filters into full text query when adding a created relative date', fakeAsync(() => {
it('should convert legacy title filters into full text query when adding a created relative date', () => {
component.filterRules = [
{
rule_type: FILTER_TITLE,
@@ -1712,9 +1717,9 @@ describe('FilterEditorComponent', () => {
value: 'foo,created:[-1 week to now]',
},
])
}))
})
it('should convert simple title filters into full text query when adding a created relative date', fakeAsync(() => {
it('should convert simple title filters into full text query when adding a created relative date', () => {
component.filterRules = [
{
rule_type: FILTER_SIMPLE_TITLE,
@@ -1734,9 +1739,9 @@ describe('FilterEditorComponent', () => {
value: 'foo,created:[-1 week to now]',
},
])
}))
})
it('should leave relative dates not in quick list intact', fakeAsync(() => {
it('should leave relative dates not in quick list intact', () => {
component.textFilterInput.nativeElement.value = 'created:[-2 week to now]'
component.textFilterInput.nativeElement.dispatchEvent(new Event('input'))
const textFieldTargetDropdown = fixture.debugElement.queryAll(
@@ -1762,9 +1767,9 @@ describe('FilterEditorComponent', () => {
value: 'added:[-2 month to now]',
},
])
}))
})
it('should convert user input to correct filter rules on date added after', fakeAsync(() => {
it('should convert user input to correct filter rules on date added after', () => {
const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent)
)
@@ -1782,9 +1787,9 @@ describe('FilterEditorComponent', () => {
value: '2023-05-14',
},
])
}))
})
it('should convert user input to correct filter rules on date added before', fakeAsync(() => {
it('should convert user input to correct filter rules on date added before', () => {
const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent)
)
@@ -1802,9 +1807,9 @@ describe('FilterEditorComponent', () => {
value: '2023-05-14',
},
])
}))
})
it('should convert user input to correct filter rules on date added with relative date', fakeAsync(() => {
it('should convert user input to correct filter rules on date added with relative date', () => {
const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent)
)
@@ -1818,9 +1823,9 @@ describe('FilterEditorComponent', () => {
value: 'added:[-1 week to now]',
},
])
}))
})
it('should carry over text filtering on date added with relative date', fakeAsync(() => {
it('should carry over text filtering on date added with relative date', () => {
component.textFilter = 'foo'
const datesDropdown = fixture.debugElement.query(
By.directive(DatesDropdownComponent)
@@ -1835,9 +1840,9 @@ describe('FilterEditorComponent', () => {
value: 'foo,added:[-1 week to now]',
},
])
}))
})
it('should convert user input to correct filter on permissions select my docs', fakeAsync(() => {
it('should convert user input to correct filter on permissions select my docs', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1851,24 +1856,9 @@ describe('FilterEditorComponent', () => {
value: '1',
},
])
}))
})
it('should convert user input to correct filter on permissions select shared with me', fakeAsync(() => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
const sharedWithMe = permissionsDropdown.queryAll(By.css('button'))[3]
sharedWithMe.triggerEventHandler('click')
fixture.detectChanges()
expect(component.filterRules).toEqual([
{
rule_type: FILTER_OWNER_DOES_NOT_INCLUDE,
value: '1',
},
])
}))
it('should convert user input to correct filter on permissions select shared with me', fakeAsync(() => {
it('should convert user input to correct filter on permissions select shared with me', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1889,9 +1879,9 @@ describe('FilterEditorComponent', () => {
value: '1,2',
},
])
}))
})
it('should convert user input to correct filter on permissions select shared by me', fakeAsync(() => {
it('should convert user input to correct filter on permissions select shared by me', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1904,9 +1894,9 @@ describe('FilterEditorComponent', () => {
value: '1',
},
])
}))
})
it('should convert user input to correct filter on permissions select unowned', fakeAsync(() => {
it('should convert user input to correct filter on permissions select unowned', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1919,9 +1909,9 @@ describe('FilterEditorComponent', () => {
value: 'true',
},
])
}))
})
it('should convert user input to correct filter on permissions select others', fakeAsync(() => {
it('should convert user input to correct filter on permissions select others', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1940,9 +1930,9 @@ describe('FilterEditorComponent', () => {
value: '3',
},
])
}))
})
it('should convert user input to correct filter on permissions hide unowned', fakeAsync(() => {
it('should convert user input to correct filter on permissions hide unowned', () => {
const permissionsDropdown = fixture.debugElement.query(
By.directive(PermissionsFilterDropdownComponent)
)
@@ -1960,7 +1950,7 @@ describe('FilterEditorComponent', () => {
value: 'false',
},
])
}))
})
// The rest
@@ -2221,7 +2211,7 @@ describe('FilterEditorComponent', () => {
})
})
it('should keep deprecated custom fields target available for legacy filters', fakeAsync(() => {
it('should keep deprecated custom fields target available for legacy filters', () => {
component.filterRules = [
{
rule_type: FILTER_CUSTOM_FIELDS_TEXT,
@@ -2242,9 +2232,9 @@ describe('FilterEditorComponent', () => {
value: 'foo',
},
])
}))
})
it('should call autocomplete endpoint on input', fakeAsync(() => {
it('should call autocomplete endpoint on input', () => {
component.textFilterTarget = 'fulltext-query' // TEXT_FILTER_TARGET_FULLTEXT_QUERY
const autocompleteSpy = jest.spyOn(searchService, 'autocomplete')
component.searchAutoComplete(of('hello')).subscribe()
@@ -2254,9 +2244,9 @@ describe('FilterEditorComponent', () => {
component.searchAutoComplete(of('hello world 1')).subscribe()
tick(250)
expect(autocompleteSpy).toHaveBeenCalled()
}))
})
it('should handle autocomplete backend failure gracefully', fakeAsync(() => {
it('should handle autocomplete backend failure gracefully', () => {
component.textFilterTarget = 'fulltext-query' // TEXT_FILTER_TARGET_FULLTEXT_QUERY
const serviceAutocompleteSpy = jest.spyOn(searchService, 'autocomplete')
serviceAutocompleteSpy.mockReturnValue(
@@ -2270,7 +2260,7 @@ describe('FilterEditorComponent', () => {
tick(250)
expect(serviceAutocompleteSpy).toHaveBeenCalled()
expect(result).toEqual([])
}))
})
it('should support choosing a autocomplete item', () => {
expect(component.textFilter).toBeNull()
@@ -1166,13 +1166,13 @@ export class FilterEditorComponent
private maybeCompleteLoading() {
this.loadingCount++
if (this.loadingCount == this.loadingCountTotal) {
this.loading = false
this.show = true
this.loading.set(false)
this.show.set(true)
}
}
ngOnInit() {
this.loading = true
this.loading.set(true)
if (
this.permissionsService.currentUserCan(
PermissionAction.View,
@@ -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>
@@ -1,9 +1,4 @@
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 { NgbActiveModal, NgbModalModule } from '@ng-bootstrap/ng-bootstrap'
@@ -22,8 +17,8 @@ describe('SaveViewConfigDialogComponent', () => {
let fixture: ComponentFixture<SaveViewConfigDialogComponent>
let modal: NgbActiveModal
beforeEach(fakeAsync(() => {
TestBed.configureTestingModule({
beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
NgbActiveModal,
{
@@ -56,16 +51,16 @@ describe('SaveViewConfigDialogComponent', () => {
fixture = TestBed.createComponent(SaveViewConfigDialogComponent)
component = fixture.componentInstance
fixture.detectChanges()
tick()
}))
await fixture.whenStable()
})
it('should support default name', () => {
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,10 +1,10 @@
import {
Component,
EventEmitter,
Input,
OnInit,
Output,
inject,
signal,
} from '@angular/core'
import {
FormControl,
@@ -32,29 +32,18 @@ import { TextComponent } from '../../common/input/text/text.component'
})
export class SaveViewConfigDialogComponent implements OnInit {
private modal = inject(NgbActiveModal)
readonly error = signal(undefined)
readonly buttonsEnabled = signal(true)
readonly defaultName = signal('')
readonly closeEnabled = signal(false)
@Output()
public saveClicked = new EventEmitter()
@Input()
error
@Input()
buttonsEnabled = true
closeEnabled = false
users: User[]
_defaultName = ''
get defaultName() {
return this._defaultName
}
@Input()
set defaultName(value: string) {
this._defaultName = value
setDefaultName(value: string) {
this.defaultName.set(value)
this.saveViewConfigForm.patchValue({ name: value })
}
@@ -68,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)
})
}