Frontend UI for creating select fields, alter extra_data format
This commit is contained in:
parent
3a20f3d75f
commit
2547f46f6b
@ -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">
|
||||||
|
@ -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 = {
|
||||||
|
@ -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 {
|
||||||
|
@ -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> <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>
|
||||||
|
@ -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)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
@ -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[]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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(
|
||||||
|
Loading…
x
Reference in New Issue
Block a user