Basic frontend

This commit is contained in:
shamoon 2024-12-04 00:20:47 -08:00
parent 9a452cc396
commit 8572d2a8cd
10 changed files with 319 additions and 15 deletions

View File

@ -133,6 +133,7 @@ import { DeletePagesConfirmDialogComponent } from './components/common/confirm-d
import { TrashComponent } from './components/admin/trash/trash.component'
import { EntriesComponent } from './components/common/input/entries/entries.component'
import { SavedViewsComponent } from './components/manage/saved-views/saved-views.component'
import { CustomFieldsBulkEditDialogComponent } from './components/document-list/bulk-editor/custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component'
import {
airplane,
archive,
@ -528,6 +529,7 @@ function initializeApp(settings: SettingsService) {
TrashComponent,
EntriesComponent,
SavedViewsComponent,
CustomFieldsBulkEditDialogComponent,
],
bootstrap: [AppComponent],
imports: [

View File

@ -66,6 +66,11 @@
</button>
}
}
@if (extraButtonTitle) {
<button class="list-group-item list-group-item-action bg-light text-primary" (click)="extraButton.next($event)" [disabled]="disabled">
<small class="ms-2 fw-bold"><ng-container i18n>{{extraButtonTitle}}</ng-container></small>
</button>
}
@if (!editing && manyToOne) {
<div class="list-group-item list-group-item-note pt-1 pb-2">
<small i18n>Click again to exclude items.</small>

View File

@ -434,21 +434,6 @@ export class FilterableDropdownComponent implements OnDestroy, OnInit {
@Input()
createRef: (name) => void
creating: boolean = false
@Output()
apply = new EventEmitter<ChangedItems>()
@Output()
opened = new EventEmitter()
get modifierToggleEnabled(): boolean {
return this.manyToOne
? this.selectionModel.selectionSize() > 1 &&
this.selectionModel.getExcludedItems().length == 0
: !this.selectionModel.isNoneSelected()
}
@Input()
set documentCounts(counts: SelectionDataItem[]) {
if (counts) {
@ -459,6 +444,27 @@ export class FilterableDropdownComponent implements OnDestroy, OnInit {
@Input()
shortcutKey: string
@Input()
extraButtonTitle: string
creating: boolean = false
@Output()
apply = new EventEmitter<ChangedItems>()
@Output()
opened = new EventEmitter()
@Output()
extraButton = new EventEmitter()
get modifierToggleEnabled(): boolean {
return this.manyToOne
? this.selectionModel.selectionSize() > 1 &&
this.selectionModel.getExcludedItems().length == 0
: !this.selectionModel.isNoneSelected()
}
get name(): string {
return this.title ? this.title.replace(/\s/g, '_').toLowerCase() : null
}

View File

@ -90,6 +90,9 @@
(opened)="openCustomFieldsDropdown()"
[(selectionModel)]="customFieldsSelectionModel"
[documentCounts]="customFieldDocumentCounts"
extraButtonTitle="Set values"
i18n-extraButtonTitle
(extraButton)="setCustomFieldValues()"
(apply)="setCustomFields($event)">
</pngx-filterable-dropdown>
}

View File

@ -1416,4 +1416,55 @@ describe('BulkEditorComponent', () => {
)
expect(component.customFields).toEqual(customFields.results)
})
it('should open the bulk edit custom field values dialog with correct parameters', () => {
let modal: NgbModalRef
modalService.activeInstances.subscribe((m) => (modal = m[0]))
jest.spyOn(permissionsService, 'currentUserCan').mockReturnValue(true)
jest
.spyOn(documentListViewService, 'documents', 'get')
.mockReturnValue([{ id: 3 }, { id: 4 }])
jest.spyOn(documentService, 'getFew').mockReturnValue(
of({
all: [3, 4],
count: 2,
results: [{ id: 3 }, { id: 4 }],
})
)
jest
.spyOn(documentListViewService, 'selected', 'get')
.mockReturnValue(new Set([3, 4]))
fixture.detectChanges()
const toastServiceShowInfoSpy = jest.spyOn(toastService, 'showInfo')
const toastServiceShowErrorSpy = jest.spyOn(toastService, 'showError')
const listReloadSpy = jest.spyOn(documentListViewService, 'reload')
component.customFields = [
{ id: 1, name: 'Custom Field 1', data_type: CustomFieldDataType.String },
{ id: 2, name: 'Custom Field 2', data_type: CustomFieldDataType.String },
]
jest
.spyOn(component.customFieldsSelectionModel, 'getSelectedItems')
.mockReturnValue([{ id: 1 }, { id: 2 }])
component.setCustomFieldValues()
expect(modal.componentInstance.customFields).toEqual(component.customFields)
expect(modal.componentInstance.selectedFieldsIds).toEqual([1, 2])
expect(modal.componentInstance.documents).toEqual([3, 4])
modal.componentInstance.failed.emit()
expect(toastServiceShowErrorSpy).toHaveBeenCalled()
expect(listReloadSpy).not.toHaveBeenCalled()
modal.componentInstance.succeeded.emit()
expect(toastServiceShowInfoSpy).toHaveBeenCalled()
expect(listReloadSpy).toHaveBeenCalled()
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=50&ordering=-created&truncate_content=true`
) // list reload
httpTestingController.match(
`${environment.apiBaseUrl}documents/?page=1&page_size=100000&fields=id`
) // listAllFilteredIds
})
})

View File

@ -44,6 +44,7 @@ import { MergeConfirmDialogComponent } from '../../common/confirm-dialog/merge-c
import { CustomField } from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { CustomFieldsBulkEditDialogComponent } from './custom-fields-bulk-edit-dialog/custom-fields-bulk-edit-dialog.component'
@Component({
selector: 'pngx-bulk-editor',
@ -826,4 +827,34 @@ export class BulkEditorComponent
)
})
}
public setCustomFieldValues() {
const modal = this.modalService.open(CustomFieldsBulkEditDialogComponent, {
backdrop: 'static',
size: 'lg',
})
const dialog =
modal.componentInstance as CustomFieldsBulkEditDialogComponent
dialog.customFields = this.customFields
dialog.selectedFieldsIds = this.customFieldsSelectionModel
.getSelectedItems()
.map((item) => item.id)
dialog.documents = Array.from(this.list.selected)
dialog.succeeded.subscribe((result) => {
this.toastService.showInfo(
$localize`Bulk operation executed successfully`
)
this.list.reload()
this.list.reduceSelectionToFilter()
this.list.selected.forEach((id) => {
this.openDocumentService.refreshDocument(id)
})
})
dialog.failed.subscribe((error) => {
this.toastService.showError(
$localize`Error executing bulk operation`,
error
)
})
}
}

View File

@ -0,0 +1,32 @@
<form [formGroup]="form" (ngSubmit)="save()" autocomplete="off">
<div class="modal-header">
<h4 class="modal-title" id="modal-basic-title">Set custom fields for {{documents?.length}} document(s)</h4>
<button type="button" class="btn-close" aria-label="Close" (click)="cancel()">
</button>
</div>
<div class="modal-body">
<pngx-input-select i18n-title title=""
multiple="true"
[items]="customFields"
[(ngModel)]="selectedFieldsIds"
placeholder="Select custom fields"
i18n-placeholder
[ngModelOptions]="{standalone: true}">
</pngx-input-select>
<div class="d-flex flex-column gap-2">
@for (field of selectedFields; track field.id) {
<div class="input-group">
<span class="input-group-text">{{field.name}}</span>
<input type="text" class="form-control" id="field_{{field.id}}" formControlName="{{field.id}}">
<button type="button" class="btn btn-outline-danger" (click)="removeField(field.id)">
<i-bs name="x"></i-bs>
</button>
</div>
}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>
<button type="submit" class="btn btn-primary" i18n [disabled]="networkActive">Save</button>
</div>
</form>

View File

@ -0,0 +1,89 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { CustomFieldsBulkEditDialogComponent } from './custom-fields-bulk-edit-dialog.component'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'
import { of, throwError } from 'rxjs'
import { DocumentService } from 'src/app/services/rest/document.service'
import { SelectComponent } from 'src/app/components/common/input/select/select.component'
import { CustomFieldDataType } from 'src/app/data/custom-field'
import { NgSelectModule } from '@ng-select/ng-select'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { provideHttpClient } from '@angular/common/http'
describe('CustomFieldsBulkEditDialogComponent', () => {
let component: CustomFieldsBulkEditDialogComponent
let fixture: ComponentFixture<CustomFieldsBulkEditDialogComponent>
let documentService: DocumentService
let activeModal: NgbActiveModal
beforeEach(async () => {
TestBed.configureTestingModule({
declarations: [CustomFieldsBulkEditDialogComponent, SelectComponent],
imports: [FormsModule, ReactiveFormsModule, NgbModule, NgSelectModule],
providers: [
NgbActiveModal,
provideHttpClient(),
provideHttpClientTesting(),
],
}).compileComponents()
fixture = TestBed.createComponent(CustomFieldsBulkEditDialogComponent)
component = fixture.componentInstance
documentService = TestBed.inject(DocumentService)
activeModal = TestBed.inject(NgbActiveModal)
fixture.detectChanges()
})
it('should initialize form controls based on selected field ids', () => {
component.customFields = [
{ id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
{ id: 2, name: 'Field 2', data_type: CustomFieldDataType.Integer },
]
component.selectedFieldsIds = [1, 2]
expect(component.form.contains('1')).toBeTruthy()
expect(component.form.contains('2')).toBeTruthy()
})
it('should emit succeeded event and close modal on successful save', () => {
const editSpy = jest
.spyOn(documentService, 'bulkEdit')
.mockReturnValue(of('Success'))
const successSpy = jest.spyOn(component.succeeded, 'emit')
component.documents = [1, 2]
component.selectedFieldsIds = [1]
component.form.controls['1'].setValue('Value 1')
component.save()
expect(editSpy).toHaveBeenCalled()
expect(successSpy).toHaveBeenCalled()
})
it('should emit failed event on save error', () => {
const editSpy = jest
.spyOn(documentService, 'bulkEdit')
.mockReturnValue(throwError(new Error('Error')))
const failSpy = jest.spyOn(component.failed, 'emit')
component.documents = [1, 2]
component.selectedFieldsIds = [1]
component.form.controls['1'].setValue('Value 1')
component.save()
expect(editSpy).toHaveBeenCalled()
expect(failSpy).toHaveBeenCalled()
})
it('should close modal on cancel', () => {
const activeModalSpy = jest.spyOn(activeModal, 'close')
component.cancel()
expect(activeModalSpy).toHaveBeenCalled()
})
it('should remove field from selected fields', () => {
component.selectedFieldsIds = [1, 2]
component.removeField(1)
expect(component.selectedFieldsIds).toEqual([2])
})
})

View File

@ -0,0 +1,85 @@
import { Component, EventEmitter, Output } from '@angular/core'
import { FormControl, FormGroup } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { first } from 'rxjs'
import { CustomField } from 'src/app/data/custom-field'
import { DocumentService } from 'src/app/services/rest/document.service'
@Component({
selector: 'pngx-custom-fields-bulk-edit-dialog',
templateUrl: './custom-fields-bulk-edit-dialog.component.html',
styleUrl: './custom-fields-bulk-edit-dialog.component.scss',
})
export class CustomFieldsBulkEditDialogComponent {
@Output()
succeeded = new EventEmitter()
@Output()
failed = new EventEmitter()
public networkActive = false
public customFields: CustomField[] = []
private _selectedFields: CustomField[] = [] // static object for change detection
public get selectedFields() {
return this._selectedFields
}
private _selectedFieldIds: number[] = []
public get selectedFieldsIds() {
return this._selectedFieldIds
}
public set selectedFieldsIds(ids: number[]) {
this._selectedFieldIds = ids
this._selectedFields = this.customFields.filter((field) =>
this._selectedFieldIds.includes(field.id)
)
this.initForm()
}
public form: FormGroup = new FormGroup({})
public documents: number[]
constructor(
private activeModal: NgbActiveModal,
private documentService: DocumentService
) {}
initForm() {
this.form = new FormGroup({})
this._selectedFieldIds.forEach((field_id) => {
this.form.addControl(field_id.toString(), new FormControl(null))
})
}
public save() {
console.log('save', this.form.value)
this.documentService
.bulkEdit(this.documents, 'modify_custom_fields', {
add_custom_fields: this.form.value,
remove_custom_fields: [],
})
.pipe(first())
.subscribe({
next: () => {
this.activeModal.close()
this.succeeded.emit()
},
error: (error) => {
this.failed.emit(error)
},
})
}
public cancel() {
this.activeModal.close()
}
public removeField(fieldId: number) {
this._selectedFieldIds = this._selectedFieldIds.filter(
(id) => id !== fieldId
)
}
}