Feature: select custom field type (#7167)

This commit is contained in:
shamoon
2024-07-09 07:57:07 -07:00
committed by GitHub
parent c03aa03ac2
commit 4ad4862641
16 changed files with 449 additions and 46 deletions

View File

@@ -30,6 +30,9 @@
<input type="checkbox" id="{{field.name}}" name="{{field.name}}" [checked]="value" value="" class="form-check-input ms-2 mt-0 pe-none">
</div>
}
@case (CustomFieldDataType.Select) {
<span [ngbTooltip]="nameTooltip">{{getSelectValue(field, value)}}</span>
}
@default {
<span [ngbTooltip]="nameTooltip">{{value}}</span>
}

View File

@@ -12,6 +12,14 @@ const customFields: CustomField[] = [
{ id: 1, name: 'Field 1', data_type: CustomFieldDataType.String },
{ id: 2, name: 'Field 2', data_type: CustomFieldDataType.Monetary },
{ id: 3, name: 'Field 3', data_type: CustomFieldDataType.DocumentLink },
{
id: 4,
name: 'Field 4',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: ['Option 1', 'Option 2', 'Option 3'],
},
},
]
const document: Document = {
id: 1,
@@ -103,4 +111,8 @@ describe('CustomFieldDisplayComponent', () => {
expect(component.currency).toEqual('EUR')
expect(component.value).toEqual(100)
})
it('should show select value', () => {
expect(component.getSelectValue(customFields[3], 2)).toEqual('Option 3')
})
})

View File

@@ -115,6 +115,10 @@ export class CustomFieldDisplayComponent implements OnInit, OnDestroy {
return this.docLinkDocuments?.find((d) => d.id === docId)?.title
}
public getSelectValue(field: CustomField, index: number): string {
return field.extra_data.select_options[index]
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()

View File

@@ -13,6 +13,23 @@
@if (typeFieldDisabled) {
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small>
}
<div [formGroup]="objectForm.controls.extra_data">
@switch (objectForm.get('data_type').value) {
@case (CustomFieldDataType.Select) {
<button type="button" class="btn btn-sm btn-primary my-2" (click)="addSelectOption()">
<span i18n>Add option</span>&nbsp;<i-bs name="plus-circle"></i-bs>
</button>
<div formArrayName="select_options">
@for (option of objectForm.controls.extra_data.controls.select_options.controls; track option; let i = $index) {
<div class="input-group input-group-sm my-2">
<input #selectOption type="text" class="form-control" [formControl]="option" autocomplete="off">
<button type="button" class="btn btn-outline-danger" (click)="removeSelectOption(i)" i18n>Delete</button>
</div>
}
</div>
}
}
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@@ -13,6 +13,9 @@ import { SelectComponent } from '../../input/select/select.component'
import { TextComponent } from '../../input/text/text.component'
import { EditDialogMode } from '../edit-dialog.component'
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { CustomFieldDataType } from 'src/app/data/custom-field'
import { ElementRef, QueryList } from '@angular/core'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
describe('CustomFieldEditDialogComponent', () => {
let component: CustomFieldEditDialogComponent
@@ -29,7 +32,13 @@ describe('CustomFieldEditDialogComponent', () => {
TextComponent,
SafeHtmlPipe,
],
imports: [FormsModule, ReactiveFormsModule, NgSelectModule, NgbModule],
imports: [
FormsModule,
ReactiveFormsModule,
NgSelectModule,
NgbModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [
NgbActiveModal,
provideHttpClient(withInterceptorsFromDi()),
@@ -63,4 +72,55 @@ describe('CustomFieldEditDialogComponent', () => {
component.ngOnInit()
expect(component.objectForm.get('data_type').disabled).toBeTruthy()
})
it('should initialize select options on edit', () => {
component.dialogMode = EditDialogMode.EDIT
component.object = {
id: 1,
name: 'Field 1',
data_type: CustomFieldDataType.Select,
extra_data: {
select_options: ['Option 1', 'Option 2', 'Option 3'],
},
}
fixture.detectChanges()
component.ngOnInit()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(3)
})
it('should support add / remove select options', () => {
component.dialogMode = EditDialogMode.CREATE
fixture.detectChanges()
component.ngOnInit()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(1)
component.addSelectOption()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(2)
component.addSelectOption()
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(3)
component.removeSelectOption(0)
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(2)
})
it('should focus on last select option input', () => {
const selectOptionInputs = component[
'selectOptionInputs'
] as QueryList<ElementRef>
component.dialogMode = EditDialogMode.CREATE
component.objectForm.get('data_type').setValue(CustomFieldDataType.Select)
component.ngOnInit()
component.ngAfterViewInit()
component.addSelectOption()
fixture.detectChanges()
expect(document.activeElement).toBe(selectOptionInputs.last.nativeElement)
})
})

View File

@@ -1,11 +1,24 @@
import { Component, OnInit } from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms'
import {
AfterViewInit,
Component,
ElementRef,
OnDestroy,
OnInit,
QueryList,
ViewChildren,
} from '@angular/core'
import { FormGroup, FormControl, FormArray } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { DATA_TYPE_LABELS, CustomField } from 'src/app/data/custom-field'
import {
DATA_TYPE_LABELS,
CustomField,
CustomFieldDataType,
} from 'src/app/data/custom-field'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service'
import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
import { Subject, takeUntil } from 'rxjs'
@Component({
selector: 'pngx-custom-field-edit-dialog',
@@ -14,8 +27,20 @@ import { EditDialogComponent, EditDialogMode } from '../edit-dialog.component'
})
export class CustomFieldEditDialogComponent
extends EditDialogComponent<CustomField>
implements OnInit
implements OnInit, AfterViewInit, OnDestroy
{
CustomFieldDataType = CustomFieldDataType
@ViewChildren('selectOption')
private selectOptionInputs: QueryList<ElementRef>
private unsubscribeNotifier: Subject<any> = new Subject()
private get selectOptions(): FormArray {
return (this.objectForm.controls.extra_data as FormGroup).controls
.select_options as FormArray
}
constructor(
service: CustomFieldsService,
activeModal: NgbActiveModal,
@@ -30,6 +55,25 @@ export class CustomFieldEditDialogComponent
if (this.typeFieldDisabled) {
this.objectForm.get('data_type').disable()
}
if (this.object?.data_type === CustomFieldDataType.Select) {
this.selectOptions.clear()
this.object.extra_data.select_options.forEach((option) =>
this.selectOptions.push(new FormControl(option))
)
}
}
ngAfterViewInit(): void {
this.selectOptionInputs.changes
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe(() => {
this.selectOptionInputs.last.nativeElement.focus()
})
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next(true)
this.unsubscribeNotifier.complete()
}
getCreateTitle() {
@@ -44,6 +88,9 @@ export class CustomFieldEditDialogComponent
return new FormGroup({
name: new FormControl(null),
data_type: new FormControl(null),
extra_data: new FormGroup({
select_options: new FormArray([new FormControl(null)]),
}),
})
}
@@ -54,4 +101,12 @@ export class CustomFieldEditDialogComponent
get typeFieldDisabled(): boolean {
return this.dialogMode === EditDialogMode.EDIT
}
public addSelectOption() {
this.selectOptions.push(new FormControl(''))
}
public removeSelectOption(index: number) {
this.selectOptions.removeAt(index)
}
}

View File

@@ -132,4 +132,12 @@ describe('SelectComponent', () => {
const expectedTitle = `Filter documents with this ${component.title}`
expect(component.filterButtonTitle).toEqual(expectedTitle)
})
it('should support setting items as a plain array', () => {
component.itemsArray = ['foo', 'bar']
expect(component.items).toEqual([
{ id: 0, name: 'foo' },
{ id: 1, name: 'bar' },
])
})
})

View File

@@ -34,6 +34,11 @@ export class SelectComponent extends AbstractInputComponent<number> {
if (items && this.value) this.checkForPrivateItems(this.value)
}
@Input()
set itemsArray(items: any[]) {
this._items = items.map((item, index) => ({ id: index, name: item }))
}
writeValue(newValue: any): void {
if (newValue && this._items) {
this.checkForPrivateItems(newValue)

View File

@@ -186,6 +186,16 @@
[horizontal]="true"
[error]="getCustomFieldError(i)"></pngx-input-document-link>
}
@case (CustomFieldDataType.Select) {
<pngx-input-select formControlName="value"
[title]="getCustomFieldFromInstance(fieldInstance)?.name"
[itemsArray]="getCustomFieldFromInstance(fieldInstance)?.extra_data.select_options"
[allowNull]="true"
[horizontal]="true"
[removable]="userIsOwner"
(removed)="removeField(fieldInstance)"
[error]="getCustomFieldError(i)"></pngx-input-select>
}
}
</div>
}

View File

@@ -9,6 +9,7 @@ export enum CustomFieldDataType {
Float = 'float',
Monetary = 'monetary',
DocumentLink = 'documentlink',
Select = 'select',
}
export const DATA_TYPE_LABELS = [
@@ -44,10 +45,17 @@ export const DATA_TYPE_LABELS = [
id: CustomFieldDataType.DocumentLink,
name: $localize`Document Link`,
},
{
id: CustomFieldDataType.Select,
name: $localize`Select`,
},
]
export interface CustomField extends ObjectWithId {
data_type: CustomFieldDataType
name: string
created?: Date
extra_data?: {
select_options?: string[]
}
}