Basic frontend
This commit is contained in:
parent
9a452cc396
commit
8572d2a8cd
@ -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: [
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -90,6 +90,9 @@
|
||||
(opened)="openCustomFieldsDropdown()"
|
||||
[(selectionModel)]="customFieldsSelectionModel"
|
||||
[documentCounts]="customFieldDocumentCounts"
|
||||
extraButtonTitle="Set values"
|
||||
i18n-extraButtonTitle
|
||||
(extraButton)="setCustomFieldValues()"
|
||||
(apply)="setCustomFields($event)">
|
||||
</pngx-filterable-dropdown>
|
||||
}
|
||||
|
@ -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
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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])
|
||||
})
|
||||
})
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user