Skip to content

Commit 07585e5

Browse files
Use a memoized handler for question type changes
1 parent 94aaba8 commit 07585e5

File tree

6 files changed

+101
-88
lines changed

6 files changed

+101
-88
lines changed

src/components/interactive-builder/modals/question/question-form/question/question.component.tsx

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import RenderTypeComponent from '../rendering-types/rendering-type.component';
55
import QuestionTypeComponent from '../question-types/question-type.component';
66
import RequiredLabel from '../common/required-label/required-label.component';
77
import { useFormField } from '../../form-field-context';
8-
import { cleanFormFieldForType } from '../../question.modal';
8+
import { cleanFormFieldForType } from '../../question-utils';
99
import type { FormField, RenderType } from '@openmrs/esm-form-engine-lib';
1010
import { questionTypes, renderTypeOptions, renderingTypes } from '@constants';
1111
import styles from './question.scss';
@@ -32,6 +32,15 @@ const Question: React.FC<QuestionProps> = ({ checkIfQuestionIdExists }) => {
3232
return checkIfQuestionIdExists(formField.id);
3333
}, [formField.id, checkIfQuestionIdExists]);
3434

35+
const handleQuestionTypeChange = useCallback(
36+
(event: React.ChangeEvent<HTMLSelectElement>) => {
37+
const newType = event.target.value;
38+
const cleaned = cleanFormFieldForType({ ...formField, type: newType }, newType);
39+
setFormField(cleaned);
40+
},
41+
[formField, setFormField],
42+
);
43+
3544
return (
3645
<>
3746
<TextInput
@@ -63,11 +72,7 @@ const Question: React.FC<QuestionProps> = ({ checkIfQuestionIdExists }) => {
6372

6473
<Select
6574
value={formField?.type ?? 'control'}
66-
onChange={(event) => {
67-
const newType = event.target.value;
68-
const cleaned = cleanFormFieldForType({ ...formField, type: newType }, newType);
69-
setFormField(cleaned);
70-
}}
75+
onChange={handleQuestionTypeChange}
7176
id="questionType"
7277
invalidText={t('typeRequired', 'Type is required')}
7378
labelText={t('questionType', 'Question type')}

src/components/interactive-builder/modals/question/question-form/question/question.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { cleanFormFieldForType } from '../../question.modal';
1+
import { cleanFormFieldForType } from '../../question-utils';
22
import type { FormField } from '@openmrs/esm-form-engine-lib';
33

44
describe('cleanFormFieldForType', () => {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { FormField } from '@openmrs/esm-form-engine-lib';
2+
import { allowedPropertiesMapping, allowedQuestionOptionsMapping } from '../../../../constants';
3+
4+
/**
5+
* Cleans the given questionOptions object by retaining only allowed keys for the new type.
6+
*/
7+
function cleanQuestionOptionsForType(options: any, newType: string): any {
8+
const allowedOpts = allowedQuestionOptionsMapping[newType] || [];
9+
const cleanedOpts = Object.fromEntries(Object.entries(options).filter(([optKey]) => allowedOpts.includes(optKey)));
10+
cleanedOpts.rendering = options.rendering;
11+
return cleanedOpts;
12+
}
13+
14+
/**
15+
* Cleans the given form field by retaining only allowed top‑level properties for the new type.
16+
* Also cleans nested questionOptions using the nested mapping.
17+
*/
18+
export function cleanFormFieldForType(field: FormField, newType: string): FormField {
19+
const allowedKeys = allowedPropertiesMapping[newType] || [];
20+
const cleaned: Partial<FormField> = {};
21+
22+
allowedKeys.forEach((key) => {
23+
if (key in field) {
24+
(cleaned as any)[key] = field[key as keyof FormField];
25+
}
26+
});
27+
28+
// If questionOptions is allowed and exists, clean it using the nested mapping.
29+
if (cleaned.questionOptions && typeof cleaned.questionOptions === 'object') {
30+
cleaned.questionOptions = cleanQuestionOptionsForType(cleaned.questionOptions, newType);
31+
}
32+
33+
cleaned.type = newType;
34+
return cleaned as FormField;
35+
}

src/components/interactive-builder/modals/question/question.modal.tsx

Lines changed: 0 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -29,86 +29,6 @@ interface QuestionModalProps {
2929
resetIndices: () => void;
3030
}
3131

32-
/**
33-
* Common properties that are required for all question types.
34-
*/
35-
const requiredProperties: Array<keyof FormField> = ['id', 'label', 'type', 'questionOptions'];
36-
37-
/**
38-
* Type-specific properties.
39-
*/
40-
const typeSpecificProperties: Record<string, Array<keyof FormField>> = {
41-
control: [],
42-
encounterDatetime: ['datePickerFormat'],
43-
encounterLocation: [],
44-
encounterProvider: [],
45-
encounterRole: [],
46-
obs: ['required'],
47-
obsGroup: ['questions'],
48-
patientIdentifier: [],
49-
testOrder: [],
50-
programState: [],
51-
};
52-
53-
/**
54-
* Merge required properties with type-specific ones.
55-
*/
56-
const allowedPropertiesMapping: Record<string, string[]> = Object.fromEntries(
57-
Object.entries(typeSpecificProperties).map(([type, props]) => {
58-
const mergedProps = new Set<string>([...requiredProperties, ...props]);
59-
return [type, Array.from(mergedProps)];
60-
}),
61-
);
62-
63-
/**
64-
* Mapping of allowed keys for the nested questionOptions object per question type.
65-
*/
66-
const allowedQuestionOptionsMapping: Record<string, string[]> = {
67-
control: ['rendering', 'minLength', 'maxLength'],
68-
encounterDatetime: ['rendering'],
69-
encounterLocation: ['rendering'],
70-
encounterProvider: ['rendering'],
71-
encounterRole: ['rendering', 'isSearchable'],
72-
obs: ['rendering', 'concept', 'answers'],
73-
obsGroup: ['rendering', 'concept'],
74-
patientIdentifier: ['rendering', 'identifierType', 'minLength', 'maxLength'],
75-
testOrder: ['rendering'],
76-
programState: ['rendering', 'programUuid', 'workflowUuid', 'answers'],
77-
};
78-
79-
/**
80-
* Cleans the given questionOptions object by retaining only allowed keys for the new type.
81-
*/
82-
function cleanQuestionOptionsForType(options: any, newType: string): any {
83-
const allowedOpts = allowedQuestionOptionsMapping[newType] || [];
84-
const cleanedOpts = Object.fromEntries(Object.entries(options).filter(([optKey]) => allowedOpts.includes(optKey)));
85-
cleanedOpts.rendering = options.rendering;
86-
return cleanedOpts;
87-
}
88-
89-
/**
90-
* Cleans the given form field by retaining only allowed top‑level properties for the new type.
91-
* Also cleans nested questionOptions using the nested mapping.
92-
*/
93-
export function cleanFormFieldForType(field: FormField, newType: string): FormField {
94-
const allowedKeys = allowedPropertiesMapping[newType] || [];
95-
const cleaned: Partial<FormField> = {};
96-
97-
allowedKeys.forEach((key) => {
98-
if (key in field) {
99-
(cleaned as any)[key] = field[key as keyof FormField];
100-
}
101-
});
102-
103-
// If questionOptions is allowed and exists, clean it using the nested mapping.
104-
if (cleaned.questionOptions && typeof cleaned.questionOptions === 'object') {
105-
cleaned.questionOptions = cleanQuestionOptionsForType(cleaned.questionOptions, newType);
106-
}
107-
108-
cleaned.type = newType;
109-
return cleaned as FormField;
110-
}
111-
11232
const getAllQuestionIds = (questions?: FormField[]): string[] => {
11333
if (!questions) return [];
11434
return flattenDeep(questions.map((question) => [question.id, getAllQuestionIds(question.questions)]));

src/constants.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { RenderType } from '@openmrs/esm-form-engine-lib';
1+
import type { FormField, RenderType } from '@openmrs/esm-form-engine-lib';
22

33
export const questionTypes = [
44
'control',
@@ -54,3 +54,50 @@ export const renderTypeOptions: Record<Exclude<QuestionType, 'obs'>, Array<Rende
5454
patientIdentifier: ['text'],
5555
programState: ['select'],
5656
};
57+
58+
/**
59+
Required properties for all question types.
60+
*/
61+
export const requiredProperties: Array<keyof FormField> = ['id', 'label', 'type', 'questionOptions'];
62+
63+
/**
64+
Type-specific properties for each question type.
65+
*/
66+
export const typeSpecificProperties: Record<string, Array<keyof FormField>> = {
67+
control: [],
68+
encounterDatetime: ['datePickerFormat'],
69+
encounterLocation: [],
70+
encounterProvider: [],
71+
encounterRole: [],
72+
obs: ['required'],
73+
obsGroup: ['questions'],
74+
patientIdentifier: [],
75+
testOrder: [],
76+
programState: [],
77+
};
78+
79+
/**
80+
Merge required properties with type-specific ones.
81+
*/
82+
export const allowedPropertiesMapping: Record<string, string[]> = Object.fromEntries(
83+
Object.entries(typeSpecificProperties).map(([type, props]) => {
84+
const mergedProps = new Set<string>([...requiredProperties, ...props]);
85+
return [type, Array.from(mergedProps)];
86+
}),
87+
);
88+
89+
/**
90+
Mapping of allowed keys for the nested questionOptions object per question type.
91+
*/
92+
export const allowedQuestionOptionsMapping: Record<string, string[]> = {
93+
control: ['rendering', 'minLength', 'maxLength'],
94+
encounterDatetime: ['rendering'],
95+
encounterLocation: ['rendering'],
96+
encounterProvider: ['rendering'],
97+
encounterRole: ['rendering', 'isSearchable'],
98+
obs: ['rendering', 'concept', 'answers'],
99+
obsGroup: ['rendering', 'concept'],
100+
patientIdentifier: ['rendering', 'identifierType', 'minLength', 'maxLength'],
101+
testOrder: ['rendering'],
102+
programState: ['rendering', 'programUuid', 'workflowUuid', 'answers'],
103+
};

translations/en.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,10 @@
118118
"loading": "Loading",
119119
"loadingPreview": "Loading preview...",
120120
"loadingSchema": "Loading schema",
121+
"loadingTranslations": "Loading translations...",
121122
"manageForms": "Manage Forms",
123+
"manageTranslation": "Manage Translation",
124+
"manageTranslations": "Manage Translations",
122125
"mappings": "Mappings",
123126
"maximizeEditor": "Maximize editor",
124127
"minimizeEditor": "Minimize editor",
@@ -181,6 +184,7 @@
181184
"saveFormToServer": "Save form to server",
182185
"saveSuccess": "was updated successfully",
183186
"saveSuccessMessage": "was created successfully. It is now visible on the Forms dashboard.",
187+
"saveTranslations": "Save Translations",
184188
"saving": "Saving",
185189
"schemaActions": "Schema actions",
186190
"schemaEditor": "Schema editor",
@@ -201,11 +205,13 @@
201205
"sectionExplainer": "A section will typically contain one or more questions. Click the button below to add a question to this section.",
202206
"sectionRenamed": "Section renamed",
203207
"selectAnswersToDisplay": "Select answers to display",
208+
"selectLanguage": "Select Language:",
204209
"selectProgram": "Select a program",
205210
"source": "Source",
206211
"startBuilding": "Start building",
207212
"success": "Success!",
208213
"timerOnly": "Timer only",
214+
"translationBuilder": "Translation Builder",
209215
"tryAgain": "Try again",
210216
"typeRequired": "Type is required",
211217
"undo": "Undo",

0 commit comments

Comments
 (0)