AAE-22948 Radio buttons should not display errors / call apis when they're in readOnly state (#9946)

* AAE-22948 Update form-field.model

* AAE-22948 Update radio-buttons.widget

* AAE-22948 Update radio-buttons-cloud.widget

* AAE-22948 Update radio-buttons-cloud

* AAE-22948 Update radio-buttons

* AAE-22948 Improvements

* AAE-22948 Small fix

* AAE-22948 Fix unit
This commit is contained in:
Wiktor Danielewski
2024-07-25 16:19:52 +02:00
committed by GitHub
parent 63d017350d
commit 5d969ccca5
7 changed files with 435 additions and 152 deletions

View File

@@ -112,17 +112,8 @@ describe('FormFieldModel', () => {
expect(field.readOnly).toBeTruthy();
});
it('should parse and leave dropdown value as is', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
options: [],
value: 'deferred'
});
expect(field.value).toBe('deferred');
});
it('should add value to field options if NOT present', () => {
describe('dropdown field model instantiation', () => {
it('should add value (selected option) to field options if NOT present', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
options: [],
@@ -133,6 +124,22 @@ describe('FormFieldModel', () => {
expect(field.value).toEqual('id_one');
});
it('should add value (selected options) to field options if NOT present (multiple selection)', () => {
const selectedOptions = [
{ id: 'id_one', name: 'One' },
{ id: 'id_two', name: 'Two' }
];
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
options: [],
value: selectedOptions,
selectionType: 'multiple'
});
expect(field.options).toEqual(selectedOptions);
expect(field.value).toEqual(selectedOptions);
});
it('should assign "empty" option as value if value is null and "empty" option is present in options', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
@@ -175,6 +182,43 @@ describe('FormFieldModel', () => {
]);
});
it('should parse dropdown with multiple options', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
options: [
{ id: 'fake-option-1', name: 'fake label 1' },
{ id: 'fake-option-2', name: 'fake label 2' },
{ id: 'fake-option-3', name: 'fake label 3' }
],
value: [],
selectionType: 'multiple'
});
expect(field.hasMultipleValues).toBe(true);
expect(field.hasEmptyValue).toBe(false);
});
describe('should leave not resolved value (in case of delayed options)', () => {
it('when string', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
options: [],
value: 'delayed-option-id'
});
expect(field.value).toBe('delayed-option-id');
});
it('when object', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
options: [],
value: { id: 'delayed-option-id', name: 'Delayed option' }
});
expect(field.value).toBe('delayed-option-id');
});
});
});
it('should store the date value as Date object if the display format is missing', () => {
const form = new FormModel();
const field = new FormFieldModel(form, {
@@ -502,41 +546,93 @@ describe('FormFieldModel', () => {
expect(field.hasEmptyValue).toBe(true);
});
it('should parse dropdown with multiple options', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.DROPDOWN,
options: [
{ id: 'fake-option-1', name: 'fake label 1' },
{ id: 'fake-option-2', name: 'fake label 2' },
{ id: 'fake-option-3', name: 'fake label 3' }
],
value: [],
selectionType: 'multiple'
});
expect(field.hasMultipleValues).toBe(true);
expect(field.hasEmptyValue).toBe(false);
});
it('should parse and resolve radio button value', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
options: [
describe('radio buttons field model instantiation', () => {
const mockOptions = [
{ id: 'opt1', name: 'Option 1' },
{ id: 'opt2', name: 'Option 2' }
],
];
describe('should parse and resolve selected option id in case of', () => {
it('string - id', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
options: mockOptions,
value: 'opt2'
});
expect(field.value).toBe('opt2');
expect(field.options).toEqual(mockOptions);
});
it('should parse and leave radio button value as is', () => {
it('string - name', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
options: mockOptions,
value: 'Option 1'
});
expect(field.value).toBe('opt1');
expect(field.options).toEqual(mockOptions);
});
it('object with id and name', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
options: mockOptions,
value: { id: 'opt2', name: 'Option 2' }
});
expect(field.value).toBe('opt2');
expect(field.options).toEqual(mockOptions);
});
it('object with id, name and options', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
options: [{ id: 'lower-priority-opt', name: 'Lower Priority Option' }],
value: {
id: 'opt2',
name: 'Option 2',
options: mockOptions
}
});
expect(field.value).toBe('opt2');
expect(field.options).toEqual(mockOptions);
});
});
describe('should leave not resolved value (in case of delayed options)', () => {
it('when string', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
options: [],
value: 'deferred-radio'
value: 'delayed-option-id'
});
expect(field.value).toBe('delayed-option-id');
});
it('when object', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
options: [],
value: { id: 'delayed-option-id', name: 'Delayed option' }
});
expect(field.value).toBe('delayed-option-id');
});
});
it('should add value (selected option) to field options if NOT present', () => {
const field = new FormFieldModel(new FormModel(), {
type: FormFieldTypes.RADIO_BUTTONS,
options: [],
value: { id: 'opt1', name: 'Option 1' }
});
expect(field.options).toEqual([{ id: 'opt1', name: 'Option 1' }]);
expect(field.value).toEqual('opt1');
});
expect(field.value).toBe('deferred-radio');
});
it('should parse boolean value when set to "true"', () => {
@@ -595,49 +691,94 @@ describe('FormFieldModel', () => {
expect(form.values['dropdown-2']).toEqual(field.options[1]);
});
it('should update form with radio button value', () => {
const form = new FormModel();
const field = new FormFieldModel(form, {
id: 'radio-1',
describe('radio buttons field value change', () => {
let form: FormModel;
let field: FormFieldModel;
describe('when rest type', () => {
beforeEach(() => {
form = new FormModel();
field = new FormFieldModel(form, {
id: 'rest-radio',
type: FormFieldTypes.RADIO_BUTTONS,
optionType: 'rest',
options: [
{ id: 'opt1', name: 'Option 1' },
{ id: 'opt2', name: 'Option 2' }
{ id: 'restOpt1', name: 'Rest Option 1' },
{ id: 'restOpt2', name: 'Rest Option 2' }
]
});
field.value = 'opt2';
expect(form.values['radio-1']).toEqual(field.options[1]);
});
it('should update form with null when radio button value does NOT match any option', () => {
const form = new FormModel();
const field = new FormFieldModel(form, {
id: 'radio-2',
type: FormFieldTypes.RADIO_BUTTONS,
options: [
{ id: 'opt1', name: 'Option 1' },
{ id: 'opt2', name: 'Option 2' }
]
it('should update form with selected option and options from which we chose', () => {
field.value = 'restOpt2';
expect(form.values['rest-radio']).toEqual({
id: 'restOpt2',
name: 'Rest Option 2',
options: field.options
});
});
field.value = 'missing';
expect(form.values['radio-2']).toBe(null);
});
it('should update form with null when radio button value is null', () => {
const form = new FormModel();
const field = new FormFieldModel(form, {
id: 'radio-2',
type: FormFieldTypes.RADIO_BUTTONS,
options: [
{ id: 'opt1', name: 'Option 1' },
{ id: 'opt2', name: 'Option 2' }
]
describe('should update form with selected option properties set to null and options from which we chose', () => {
it('when value does NOT match any option', () => {
field.value = 'not_exist';
expect(form.values['rest-radio']).toEqual({
id: null,
name: null,
options: field.options
});
});
it('when radio button value is null', () => {
field.value = null;
expect(form.values['radio-2']).toBe(null);
expect(form.values['rest-radio']).toEqual({
id: null,
name: null,
options: field.options
});
});
});
});
describe('when manual type', () => {
beforeEach(() => {
form = new FormModel();
field = new FormFieldModel(form, {
id: 'manual-radio',
type: FormFieldTypes.RADIO_BUTTONS,
optionType: 'manual',
options: [
{ id: 'opt1', name: 'Static Option 1' },
{ id: 'opt2', name: 'Static Option 2' }
]
});
});
it('should update form with selected option', () => {
field.value = 'opt1';
expect(form.values['manual-radio']).toEqual({
id: 'opt1',
name: 'Static Option 1'
});
});
describe('should update form with selected option set to null', () => {
it('when value does NOT match any option', () => {
field.value = 'not_exist';
expect(form.values['manual-radio']).toEqual(null);
});
it('when radio button value is null', () => {
field.value = null;
expect(form.values['manual-radio']).toEqual(null);
});
});
});
});
it('should not update form with display-only field value', () => {

View File

@@ -322,7 +322,7 @@ export class FormFieldModel extends FormWidgetModel {
}
if (this.isValidOption(value)) {
this.addOption(value);
this.addOption({ id: value.id, name: value.name });
return value.id;
}
@@ -341,14 +341,27 @@ export class FormFieldModel extends FormWidgetModel {
but saving back as object: { id: <id>, name: <name> }
*/
if (json.type === FormFieldTypes.RADIO_BUTTONS) {
if (json.value?.options) {
this.options = this.parseValidOptions(json.value.options);
}
// Activiti has a bug with default radio button value where initial selection passed as `name` value
// so try resolving current one with a fallback to first entry via name or id
// TODO: needs to be reported and fixed at Activiti side
const entry: FormFieldOption[] = this.options.filter(
const matchingOption = this.options.find(
(opt) => opt.id === value || opt.name === value || (value && (opt.id === value.id || opt.name === value.name))
);
return entry.length > 0 ? entry[0].id : value;
if (matchingOption) {
return matchingOption.id;
}
if (this.isValidOption(value)) {
this.addOption({ id: value.id, name: value.name });
return value.id;
}
return value;
}
if (this.isDateField(json) || this.isDateTimeField(json)) {
@@ -414,7 +427,15 @@ export class FormFieldModel extends FormWidgetModel {
}
case FormFieldTypes.RADIO_BUTTONS: {
const radioButton: FormFieldOption = this.options.find((opt) => opt.id === this.value);
this.form.values[this.id] = radioButton || null;
if (this.optionType === 'rest') {
this.form.values[this.id] = radioButton
? { ...radioButton, options: this.options }
: { id: null, name: null, options: this.options };
} else {
this.form.values[this.id] = radioButton ? { ...radioButton } : null;
}
break;
}
case FormFieldTypes.UPLOAD: {

View File

@@ -81,31 +81,73 @@ describe('RadioButtonsCloudWidgetComponent', () => {
expect(widget.field.value).toEqual('fake-opt');
});
it('should show radio buttons as text when is readonly', async () => {
describe('when widget is readonly', () => {
beforeEach(() => {
widget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name',
type: FormFieldTypes.RADIO_BUTTONS,
readOnly: true
});
fixture.detectChanges();
await fixture.whenStable();
fixture.detectChanges();
});
it('should show radio buttons as text', () => {
expect(element.querySelector('display-text-widget')).toBeDefined();
});
it('should be able to set label property for Radio Button widget', () => {
widget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name-label',
type: FormFieldTypes.RADIO_BUTTONS,
readOnly: true
it('should set label property', () => {
expect(element.querySelector('label').innerText).toBe('radio-name');
});
fixture.detectChanges();
expect(element.querySelector('label').innerText).toBe('radio-name-label');
});
it('should be able to set a Radio Button widget as required', async () => {
describe('fetching options from rest api', () => {
const getRadioButtonsWidgetConfig = (readOnly: boolean) => ({
id: 'rest-radio-id',
name: 'Rest Radio Buttons',
type: FormFieldTypes.RADIO_BUTTONS,
readOnly,
optionType: 'rest',
restUrl: '<url>'
});
beforeEach(() => {
spyOn(formCloudService, 'getRestWidgetData').and.returnValue(of(restOption));
});
describe('when widget is readonly', () => {
it('should call rest api when form is NOT readonly', () => {
widget.field = new FormFieldModel(new FormModel({}, undefined, false), getRadioButtonsWidgetConfig(true));
fixture.detectChanges();
expect(formCloudService.getRestWidgetData).toHaveBeenCalled();
});
it('should NOT call rest api when form is readonly', () => {
widget.field = new FormFieldModel(new FormModel({}, undefined, true), getRadioButtonsWidgetConfig(true));
fixture.detectChanges();
expect(formCloudService.getRestWidgetData).not.toHaveBeenCalled();
});
});
describe('when widget is NOT readonly', () => {
it('should call rest api when form is NOT readonly', () => {
widget.field = new FormFieldModel(new FormModel({}, undefined, false), getRadioButtonsWidgetConfig(false));
fixture.detectChanges();
expect(formCloudService.getRestWidgetData).toHaveBeenCalled();
});
it('should NOT call rest api when form is readonly', () => {
widget.field = new FormFieldModel(new FormModel({}, undefined, true), getRadioButtonsWidgetConfig(false));
fixture.detectChanges();
expect(formCloudService.getRestWidgetData).not.toHaveBeenCalled();
});
});
});
it('should be able to set a Radio Buttons widget as required', async () => {
widget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name-label',
@@ -131,7 +173,7 @@ describe('RadioButtonsCloudWidgetComponent', () => {
expect(widget.field.isValid).toBe(true);
});
it('should set Radio Button as valid when required and not empty', async () => {
it('should set Radio Buttons widget as valid when required and not empty', async () => {
widget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name-label',
@@ -149,7 +191,7 @@ describe('RadioButtonsCloudWidgetComponent', () => {
expect(widget.field.isValid).toBe(true);
});
it('should be able to set a Radio Button widget when rest option enabled', () => {
it('should be able to set a Radio Buttons widget when rest option enabled', () => {
spyOn(formCloudService, 'getRestWidgetData').and.returnValue(of(restOption));
widget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',

View File

@@ -52,7 +52,7 @@ export class RadioButtonsCloudWidgetComponent extends WidgetComponent implements
}
ngOnInit() {
if (this.field?.restUrl) {
if (this.field?.restUrl && !this.field?.form?.readOnly) {
this.getValuesFromRestApi();
}
}

View File

@@ -960,8 +960,7 @@ describe('FormComponent', () => {
radioField = formFields.find((field) => field.id === 'radio');
expect(dropdownField.value).toBe('dropdown_option_2');
expect(radioField.value.id).toBe('radio_option_3');
expect(radioField.value.name).toBe('Radio option 3');
expect(radioField.value).toBe('radio_option_3');
});
it('should refresh radio buttons value when id is given to data', () => {

View File

@@ -166,6 +166,86 @@ describe('RadioButtonsWidgetComponent', () => {
expect(widget.field.value).toEqual('fake-opt');
});
describe('fetching options from rest api', () => {
const getRadioButtonsWidgetConfig = (readOnly: boolean) => ({
id: 'rest-radio-id',
name: 'Rest Radio Buttons',
type: FormFieldTypes.RADIO_BUTTONS,
readOnly,
optionType: 'rest',
restUrl: '<url>'
});
describe('when NOT readonly Radio Buttons widget is part of readonly form', () => {
it('should NOT request field values from service by task id', () => {
widget.field = new FormFieldModel(new FormModel({ taskId: '<task-id>' }, undefined, true), getRadioButtonsWidgetConfig(false));
spyOn(taskFormService, 'getRestFieldValues').and.returnValue(of([]));
widget.ngOnInit();
expect(taskFormService.getRestFieldValues).not.toHaveBeenCalled();
});
it('should NOT request field values from service by process definition id', () => {
widget.field = new FormFieldModel(
new FormModel({ processDefinitionId: '<definition-id>' }, undefined, true),
getRadioButtonsWidgetConfig(false)
);
spyOn(processDefinitionService, 'getRestFieldValuesByProcessId').and.returnValue(of([]));
widget.ngOnInit();
expect(processDefinitionService.getRestFieldValuesByProcessId).not.toHaveBeenCalled();
});
});
describe('when NOT readonly Radio Buttons widget is part of NOT readonly form', () => {
it('should request field values from service by task id', () => {
widget.field = new FormFieldModel(new FormModel({ taskId: '<task-id>' }, undefined, false), getRadioButtonsWidgetConfig(false));
spyOn(taskFormService, 'getRestFieldValues').and.returnValue(of([]));
widget.ngOnInit();
expect(taskFormService.getRestFieldValues).toHaveBeenCalled();
});
it('should request field values from service by process definition id', () => {
widget.field = new FormFieldModel(
new FormModel({ processDefinitionId: '<definition-id>' }, undefined, false),
getRadioButtonsWidgetConfig(false)
);
spyOn(processDefinitionService, 'getRestFieldValuesByProcessId').and.returnValue(of([]));
widget.ngOnInit();
expect(processDefinitionService.getRestFieldValuesByProcessId).toHaveBeenCalled();
});
});
describe('when readonly Radio Buttons widget is part of NOT readonly form', () => {
it('should request field values from service by task id', () => {
widget.field = new FormFieldModel(new FormModel({ taskId: '<task-id>' }, undefined, false), getRadioButtonsWidgetConfig(true));
spyOn(taskFormService, 'getRestFieldValues').and.returnValue(of([]));
widget.ngOnInit();
expect(taskFormService.getRestFieldValues).toHaveBeenCalled();
});
it('should request field values from service by process definition id', () => {
widget.field = new FormFieldModel(
new FormModel({ processDefinitionId: '<definition-id>' }, undefined, false),
getRadioButtonsWidgetConfig(true)
);
spyOn(processDefinitionService, 'getRestFieldValuesByProcessId').and.returnValue(of([]));
widget.ngOnInit();
expect(processDefinitionService.getRestFieldValuesByProcessId).toHaveBeenCalled();
});
});
});
describe('when template is ready', () => {
let radioButtonWidget: RadioButtonsWidgetComponent;
let fixture: ComponentFixture<RadioButtonsWidgetComponent>;
@@ -190,7 +270,7 @@ describe('RadioButtonsWidgetComponent', () => {
loader = TestbedHarnessEnvironment.loader(fixture);
});
it('should show radio buttons as text when is readonly', async () => {
it('should show Radio Buttons as text when is readonly', async () => {
radioButtonWidget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name',
@@ -202,7 +282,7 @@ describe('RadioButtonsWidgetComponent', () => {
expect(element.querySelector('display-text-widget')).toBeDefined();
});
it('should be able to set label property for Radio Button widget', () => {
it('should be able to set label property for Radio Buttons widget', () => {
radioButtonWidget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name-label',
@@ -213,7 +293,7 @@ describe('RadioButtonsWidgetComponent', () => {
expect(element.querySelector('label').innerText).toBe('radio-name-label');
});
it('should be able to set a Radio Button widget as required', async () => {
it('should be able to set a Radio Buttons widget as required', async () => {
radioButtonWidget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name-label',
@@ -238,7 +318,7 @@ describe('RadioButtonsWidgetComponent', () => {
expect(radioButtonWidget.field.isValid).toBe(true);
});
it('should be able to set another Radio Button widget as required', async () => {
it('should be able to set another Radio Buttons widget as required', async () => {
radioButtonWidget.field = new FormFieldModel(new FormModel({}), {
id: 'radio-id',
name: 'radio-name-label',
@@ -277,7 +357,7 @@ describe('RadioButtonsWidgetComponent', () => {
expect(tooltip).toEqual(radioButtonWidget.field.tooltip);
});
describe('and radioButton is populated via taskId', () => {
describe('and Radio Buttons widget is populated via taskId', () => {
beforeEach(() => {
spyOn(taskFormService, 'getRestFieldValues').and.returnValue(of(restOption));
radioButtonWidget.field = new FormFieldModel(new FormModel({ taskId: 'task-id' }), {
@@ -311,7 +391,7 @@ describe('RadioButtonsWidgetComponent', () => {
});
}));
describe('and radioButton is readonly', () => {
describe('and Radio Buttons widget is readonly', () => {
beforeEach(() => {
radioButtonWidget.field.readOnly = true;
fixture.detectChanges();
@@ -341,7 +421,7 @@ describe('RadioButtonsWidgetComponent', () => {
});
});
describe('and radioButton is populated via processDefinitionId', () => {
describe('and Radio Buttons widget is populated via processDefinitionId', () => {
beforeEach(() => {
radioButtonWidget.field = new FormFieldModel(new FormModel({ processDefinitionId: 'proc-id' }), {
id: 'radio-id',

View File

@@ -55,7 +55,7 @@ export class RadioButtonsWidgetComponent extends WidgetComponent implements OnIn
}
ngOnInit() {
if (this.field?.restUrl) {
if (this.field?.restUrl && !this.field?.form?.readOnly) {
if (this.field.form.taskId) {
this.getOptionsByTaskId();
} else {