Frontend UI for creating select fields, alter extra_data format

This commit is contained in:
shamoon 2024-07-05 17:55:49 -07:00
parent 3a20f3d75f
commit 2547f46f6b
10 changed files with 147 additions and 19 deletions

View File

@ -544,7 +544,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">19</context> <context context-type="linenumber">36</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
@ -1447,6 +1447,10 @@
<context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.html</context> <context context-type="sourcefile">src/app/components/admin/users-groups/users-groups.component.html</context>
<context context-type="linenumber">76</context> <context context-type="linenumber">76</context>
</context-group> </context-group>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">26</context>
</context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component.ts</context>
<context context-type="linenumber">53</context> <context context-type="linenumber">53</context>
@ -1624,7 +1628,7 @@
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">18</context> <context context-type="linenumber">35</context>
</context-group> </context-group>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component.html</context>
@ -3445,18 +3449,25 @@
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="4910631867841099191" datatype="html">
<source>Add option</source>
<context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.html</context>
<context context-type="linenumber">20</context>
</context-group>
</trans-unit>
<trans-unit id="528950215505228201" datatype="html"> <trans-unit id="528950215505228201" datatype="html">
<source>Create new custom field</source> <source>Create new custom field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context>
<context context-type="linenumber">36</context> <context context-type="linenumber">44</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="8751213029607178010" datatype="html"> <trans-unit id="8751213029607178010" datatype="html">
<source>Edit custom field</source> <source>Edit custom field</source>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context> <context context-type="sourcefile">src/app/components/common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component.ts</context>
<context context-type="linenumber">40</context> <context context-type="linenumber">48</context>
</context-group> </context-group>
</trans-unit> </trans-unit>
<trans-unit id="6672809941092516947" datatype="html"> <trans-unit id="6672809941092516947" datatype="html">

View File

@ -16,7 +16,9 @@ const customFields: CustomField[] = [
id: 4, id: 4,
name: 'Field 4', name: 'Field 4',
data_type: CustomFieldDataType.Select, data_type: CustomFieldDataType.Select,
extra_data: ['Option 1', 'Option 2', 'Option 3'], extra_data: {
select_options: ['Option 1', 'Option 2', 'Option 3'],
},
}, },
] ]
const document: Document = { const document: Document = {

View File

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

View File

@ -13,6 +13,23 @@
@if (typeFieldDisabled) { @if (typeFieldDisabled) {
<small class="d-block mt-n2" i18n>Data type cannot be changed after a field is created</small> <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 mb-2" (click)="addSelectOption()">
<span i18n>Add option</span>&nbsp;<i class="bi bi-plus"></i>
</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 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>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button> <button type="button" class="btn btn-outline-secondary" (click)="cancel()" i18n [disabled]="networkActive">Cancel</button>

View File

@ -63,4 +63,25 @@ describe('CustomFieldEditDialogComponent', () => {
component.ngOnInit() component.ngOnInit()
expect(component.objectForm.get('data_type').disabled).toBeTruthy() expect(component.objectForm.get('data_type').disabled).toBeTruthy()
}) })
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(0)
component.addSelectOption()
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.removeSelectOption(0)
expect(
component.objectForm.get('extra_data').get('select_options').value.length
).toBe(1)
})
}) })

View File

@ -1,7 +1,11 @@
import { Component, OnInit } from '@angular/core' import { Component, OnInit } from '@angular/core'
import { FormGroup, FormControl } from '@angular/forms' import { FormGroup, FormControl, FormArray } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap' 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 { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { UserService } from 'src/app/services/rest/user.service' import { UserService } from 'src/app/services/rest/user.service'
import { SettingsService } from 'src/app/services/settings.service' import { SettingsService } from 'src/app/services/settings.service'
@ -16,6 +20,13 @@ export class CustomFieldEditDialogComponent
extends EditDialogComponent<CustomField> extends EditDialogComponent<CustomField>
implements OnInit implements OnInit
{ {
CustomFieldDataType = CustomFieldDataType
private get selectOptions(): FormArray {
return (this.objectForm.controls.extra_data as FormGroup).controls
.select_options as FormArray
}
constructor( constructor(
service: CustomFieldsService, service: CustomFieldsService,
activeModal: NgbActiveModal, activeModal: NgbActiveModal,
@ -44,6 +55,9 @@ export class CustomFieldEditDialogComponent
return new FormGroup({ return new FormGroup({
name: new FormControl(null), name: new FormControl(null),
data_type: new FormControl(null), data_type: new FormControl(null),
extra_data: new FormGroup({
select_options: new FormArray([]),
}),
}) })
} }
@ -54,4 +68,12 @@ export class CustomFieldEditDialogComponent
get typeFieldDisabled(): boolean { get typeFieldDisabled(): boolean {
return this.dialogMode === EditDialogMode.EDIT return this.dialogMode === EditDialogMode.EDIT
} }
public addSelectOption() {
this.selectOptions.push(new FormControl(''))
}
public removeSelectOption(index: number) {
this.selectOptions.removeAt(index)
}
} }

View File

@ -189,7 +189,7 @@
@case (CustomFieldDataType.Select) { @case (CustomFieldDataType.Select) {
<pngx-input-select formControlName="value" <pngx-input-select formControlName="value"
[title]="getCustomFieldFromInstance(fieldInstance)?.name" [title]="getCustomFieldFromInstance(fieldInstance)?.name"
[itemsArray]="getCustomFieldFromInstance(fieldInstance)?.extra_data" [itemsArray]="getCustomFieldFromInstance(fieldInstance)?.extra_data.select_options"
[allowNull]="true" [allowNull]="true"
[horizontal]="true" [horizontal]="true"
[removable]="userIsOwner" [removable]="userIsOwner"

View File

@ -55,5 +55,7 @@ export interface CustomField extends ObjectWithId {
data_type: CustomFieldDataType data_type: CustomFieldDataType
name: string name: string
created?: Date created?: Date
extra_data?: any extra_data?: {
select_options?: string[]
}
} }

View File

@ -480,10 +480,14 @@ class CustomFieldSerializer(serializers.ModelSerializer):
if ( if (
"data_type" in attrs "data_type" in attrs
and attrs["data_type"] == CustomField.FieldDataType.SELECT and attrs["data_type"] == CustomField.FieldDataType.SELECT
and ("extra_data" not in attrs or not isinstance(attrs["extra_data"], list)) and (
"extra_data" not in attrs
or "select_options" not in attrs["extra_data"]
or not isinstance(attrs["extra_data"]["select_options"], list)
)
): ):
raise serializers.ValidationError( raise serializers.ValidationError(
{"error": "extra_data must be a list"}, {"error": "extra_data.select_options must be a list"},
) )
return super().validate(attrs) return super().validate(attrs)
@ -574,11 +578,12 @@ class CustomFieldInstanceSerializer(serializers.ModelSerializer):
elif field.data_type == CustomField.FieldDataType.STRING: elif field.data_type == CustomField.FieldDataType.STRING:
MaxLengthValidator(limit_value=128)(data["value"]) MaxLengthValidator(limit_value=128)(data["value"])
elif field.data_type == CustomField.FieldDataType.SELECT: elif field.data_type == CustomField.FieldDataType.SELECT:
select_options = field.extra_data["select_options"]
try: try:
field.extra_data[data["value"]] select_options[data["value"]]
except Exception: except Exception:
raise serializers.ValidationError( raise serializers.ValidationError(
f"Value must be index of an element in {field.extra_data}", f"Value must be index of an element in {select_options}",
) )
return data return data

View File

@ -59,7 +59,9 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
{ {
"data_type": "select", "data_type": "select",
"name": "Select Field", "name": "Select Field",
"extra_data": ["Option 1", "Option 2"], "extra_data": {
"select_options": ["Option 1", "Option 2"],
},
}, },
), ),
content_type="application/json", content_type="application/json",
@ -68,7 +70,10 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
data = resp.json() data = resp.json()
self.assertCountEqual(data["extra_data"], ["Option 1", "Option 2"]) self.assertCountEqual(
data["extra_data"]["select_options"],
["Option 1", "Option 2"],
)
def test_create_custom_field_nonunique_name(self): def test_create_custom_field_nonunique_name(self):
""" """
@ -93,6 +98,45 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
) )
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST) self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_custom_field_select_invalid_options(self):
"""
GIVEN:
- Custom field does not exist
WHEN:
- API request to create custom field with invalid select options
THEN:
- HTTP 400 is returned
"""
# Not a list
resp = self.client.post(
self.ENDPOINT,
json.dumps(
{
"data_type": "select",
"name": "Select Field",
"extra_data": {
"select_options": "not a list",
},
},
),
content_type="application/json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
# No options
resp = self.client.post(
self.ENDPOINT,
json.dumps(
{
"data_type": "select",
"name": "Select Field",
},
),
content_type="application/json",
)
self.assertEqual(resp.status_code, status.HTTP_400_BAD_REQUEST)
def test_create_custom_field_instance(self): def test_create_custom_field_instance(self):
""" """
GIVEN: GIVEN:
@ -155,7 +199,9 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
custom_field_select = CustomField.objects.create( custom_field_select = CustomField.objects.create(
name="Test Custom Field Select", name="Test Custom Field Select",
data_type=CustomField.FieldDataType.SELECT, data_type=CustomField.FieldDataType.SELECT,
extra_data=["Option 1", "Option 2"], extra_data={
"select_options": ["Option 1", "Option 2"],
},
) )
date_value = date.today() date_value = date.today()
@ -614,7 +660,9 @@ class TestCustomFieldsAPI(DirectoriesMixin, APITestCase):
custom_field_select = CustomField.objects.create( custom_field_select = CustomField.objects.create(
name="Test Custom Field SELECT", name="Test Custom Field SELECT",
data_type=CustomField.FieldDataType.SELECT, data_type=CustomField.FieldDataType.SELECT,
extra_data=["Option 1", "Option 2"], extra_data={
"select_options": ["Option 1", "Option 2"],
},
) )
resp = self.client.patch( resp = self.client.patch(