@openmrs/esm-form-builder-app 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/LICENSE +401 -0
  2. package/README.md +35 -0
  3. package/package.json +106 -0
  4. package/src/components/action-buttons/action-buttons.component.tsx +185 -0
  5. package/src/components/action-buttons/action-buttons.scss +16 -0
  6. package/src/components/dashboard/dashboard.component.tsx +309 -0
  7. package/src/components/dashboard/dashboard.scss +112 -0
  8. package/src/components/dashboard/dashboard.test.tsx +208 -0
  9. package/src/components/empty-state/empty-data-illustration.component.tsx +51 -0
  10. package/src/components/empty-state/empty-state.component.tsx +41 -0
  11. package/src/components/empty-state/empty-state.scss +55 -0
  12. package/src/components/error-state/error-state.component.tsx +37 -0
  13. package/src/components/error-state/error-state.scss +49 -0
  14. package/src/components/form-editor/form-editor.component.tsx +125 -0
  15. package/src/components/form-editor/form-editor.scss +33 -0
  16. package/src/components/form-renderer/form-renderer.component.tsx +123 -0
  17. package/src/components/form-renderer/form-renderer.scss +57 -0
  18. package/src/components/interactive-builder/add-question-modal.component.tsx +427 -0
  19. package/src/components/interactive-builder/delete-page-modal.component.tsx +89 -0
  20. package/src/components/interactive-builder/delete-question-modal.component.tsx +93 -0
  21. package/src/components/interactive-builder/delete-section-modal.component.tsx +91 -0
  22. package/src/components/interactive-builder/edit-question-modal.component.tsx +465 -0
  23. package/src/components/interactive-builder/editable-value.component.tsx +64 -0
  24. package/src/components/interactive-builder/editable-value.scss +23 -0
  25. package/src/components/interactive-builder/interactive-builder.component.tsx +569 -0
  26. package/src/components/interactive-builder/interactive-builder.scss +100 -0
  27. package/src/components/interactive-builder/new-form-modal.component.tsx +86 -0
  28. package/src/components/interactive-builder/page-modal.component.tsx +91 -0
  29. package/src/components/interactive-builder/question-modal.scss +35 -0
  30. package/src/components/interactive-builder/section-modal.component.tsx +94 -0
  31. package/src/components/interactive-builder/value-editor.component.tsx +55 -0
  32. package/src/components/interactive-builder/value-editor.scss +10 -0
  33. package/src/components/modals/save-form.component.tsx +310 -0
  34. package/src/components/modals/save-form.scss +5 -0
  35. package/src/components/schema-editor/schema-editor.component.tsx +191 -0
  36. package/src/components/schema-editor/schema-editor.scss +26 -0
  37. package/src/config-schema.ts +47 -0
  38. package/src/constants.ts +3 -0
  39. package/src/declarations.d.tsx +2 -0
  40. package/src/form-builder-app-menu-link.component.tsx +13 -0
  41. package/src/forms.resource.ts +178 -0
  42. package/src/hooks/useClobdata.ts +20 -0
  43. package/src/hooks/useConceptLookup.ts +18 -0
  44. package/src/hooks/useConceptName.ts +18 -0
  45. package/src/hooks/useEncounterTypes.ts +18 -0
  46. package/src/hooks/useForm.ts +18 -0
  47. package/src/hooks/useForms.ts +20 -0
  48. package/src/index.ts +70 -0
  49. package/src/root.component.tsx +19 -0
  50. package/src/setup-tests.ts +11 -0
  51. package/src/test-helpers.tsx +37 -0
  52. package/src/types.ts +132 -0
@@ -0,0 +1,93 @@
1
+ import React from "react";
2
+ import { useTranslation } from "react-i18next";
3
+ import {
4
+ Button,
5
+ ComposedModal,
6
+ ModalBody,
7
+ ModalFooter,
8
+ ModalHeader,
9
+ } from "@carbon/react";
10
+ import { showNotification, showToast } from "@openmrs/esm-framework";
11
+ import { Schema } from "../../types";
12
+
13
+ type DeleteQuestionModal = {
14
+ onModalChange: (showModal: boolean) => void;
15
+ onSchemaChange: (schema: Schema) => void;
16
+ resetIndices: () => void;
17
+ pageIndex: number;
18
+ sectionIndex: number;
19
+ questionIndex: number;
20
+ schema: Schema;
21
+ showModal: boolean;
22
+ };
23
+
24
+ const DeleteQuestionModal: React.FC<DeleteQuestionModal> = ({
25
+ onModalChange,
26
+ onSchemaChange,
27
+ resetIndices,
28
+ pageIndex,
29
+ sectionIndex,
30
+ questionIndex,
31
+ schema,
32
+ showModal,
33
+ }) => {
34
+ const { t } = useTranslation();
35
+
36
+ const deleteQuestion = (pageIndex, sectionIndex, questionIndex) => {
37
+ try {
38
+ schema.pages[pageIndex].sections[sectionIndex].questions.splice(
39
+ questionIndex,
40
+ 1
41
+ );
42
+
43
+ onSchemaChange({ ...schema });
44
+ resetIndices();
45
+
46
+ showToast({
47
+ title: t("success", "Success!"),
48
+ kind: "success",
49
+ critical: true,
50
+ description: t("QuestionDeleted", "Question deleted"),
51
+ });
52
+ } catch (error) {
53
+ showNotification({
54
+ title: t("errorDeletingQuestion", "Error deleting question"),
55
+ kind: "error",
56
+ critical: true,
57
+ description: error?.message,
58
+ });
59
+ }
60
+ };
61
+
62
+ return (
63
+ <ComposedModal open={showModal} onClose={() => onModalChange(false)}>
64
+ <ModalHeader
65
+ title={t(
66
+ "deleteQuestionConfirmation",
67
+ "Are you sure you want to delete this question?"
68
+ )}
69
+ />
70
+ <ModalBody>
71
+ <p>
72
+ {t("deleteQuestionExplainerText", "This action cannot be undone.")}
73
+ </p>
74
+ </ModalBody>
75
+ <ModalFooter>
76
+ <Button kind="secondary" onClick={() => onModalChange(false)}>
77
+ {t("cancel", "Cancel")}
78
+ </Button>
79
+ <Button
80
+ kind="danger"
81
+ onClick={() => {
82
+ deleteQuestion(pageIndex, sectionIndex, questionIndex);
83
+ onModalChange(false);
84
+ }}
85
+ >
86
+ <span>{t("deleteQuestion", "Delete question")}</span>
87
+ </Button>
88
+ </ModalFooter>
89
+ </ComposedModal>
90
+ );
91
+ };
92
+
93
+ export default DeleteQuestionModal;
@@ -0,0 +1,91 @@
1
+ import React from "react";
2
+ import { useTranslation } from "react-i18next";
3
+ import {
4
+ Button,
5
+ ComposedModal,
6
+ ModalBody,
7
+ ModalFooter,
8
+ ModalHeader,
9
+ } from "@carbon/react";
10
+ import { showNotification, showToast } from "@openmrs/esm-framework";
11
+ import { Schema } from "../../types";
12
+
13
+ type DeleteSectionModal = {
14
+ onModalChange: (showModal: boolean) => void;
15
+ onSchemaChange: (schema: Schema) => void;
16
+ resetIndices: () => void;
17
+ pageIndex: number;
18
+ sectionIndex: number;
19
+ schema: Schema;
20
+ showModal: boolean;
21
+ };
22
+
23
+ const DeleteSectionModal: React.FC<DeleteSectionModal> = ({
24
+ onModalChange,
25
+ onSchemaChange,
26
+ resetIndices,
27
+ pageIndex,
28
+ sectionIndex,
29
+ schema,
30
+ showModal,
31
+ }) => {
32
+ const { t } = useTranslation();
33
+
34
+ const deleteSection = (pageIndex, sectionIndex) => {
35
+ try {
36
+ schema.pages[pageIndex].sections.splice(sectionIndex, 1);
37
+
38
+ onSchemaChange({ ...schema });
39
+ resetIndices();
40
+
41
+ showToast({
42
+ title: t("success", "Success!"),
43
+ kind: "success",
44
+ critical: true,
45
+ description: t("SectionDeleted", "Section deleted"),
46
+ });
47
+ } catch (error) {
48
+ showNotification({
49
+ title: t("errorDeletingSection", "Error deleting section"),
50
+ kind: "error",
51
+ critical: true,
52
+ description: error?.message,
53
+ });
54
+ }
55
+ };
56
+
57
+ return (
58
+ <ComposedModal open={showModal} onClose={() => onModalChange(false)}>
59
+ <ModalHeader
60
+ title={t(
61
+ "deleteSectionConfirmation",
62
+ "Are you sure you want to delete this section?"
63
+ )}
64
+ />
65
+ <ModalBody>
66
+ <p>
67
+ {t(
68
+ "deleteSectionExplainerText",
69
+ "Deleting this section will delete all the questions associated with it. This action cannot be undone."
70
+ )}
71
+ </p>
72
+ </ModalBody>
73
+ <ModalFooter>
74
+ <Button kind="secondary" onClick={() => onModalChange(false)}>
75
+ {t("cancel", "Cancel")}
76
+ </Button>
77
+ <Button
78
+ kind="danger"
79
+ onClick={() => {
80
+ deleteSection(pageIndex, sectionIndex);
81
+ onModalChange(false);
82
+ }}
83
+ >
84
+ <span>{t("deleteSection", "Delete section")}</span>
85
+ </Button>
86
+ </ModalFooter>
87
+ </ComposedModal>
88
+ );
89
+ };
90
+
91
+ export default DeleteSectionModal;
@@ -0,0 +1,465 @@
1
+ import React, { useState } from "react";
2
+ import { useTranslation } from "react-i18next";
3
+ import {
4
+ Button,
5
+ ComposedModal,
6
+ Form,
7
+ FormGroup,
8
+ FormLabel,
9
+ Layer,
10
+ InlineLoading,
11
+ ModalBody,
12
+ ModalFooter,
13
+ ModalHeader,
14
+ MultiSelect,
15
+ RadioButton,
16
+ RadioButtonGroup,
17
+ Search,
18
+ Select,
19
+ SelectItem,
20
+ Stack,
21
+ Tag,
22
+ TextInput,
23
+ Tile,
24
+ } from "@carbon/react";
25
+ import { flattenDeep } from "lodash-es";
26
+ import { showNotification, showToast, useConfig } from "@openmrs/esm-framework";
27
+ import {
28
+ Answer,
29
+ Concept,
30
+ ConceptMapping,
31
+ FieldTypes,
32
+ Question,
33
+ Schema,
34
+ } from "../../types";
35
+ import { useConceptLookup } from "../../hooks/useConceptLookup";
36
+ import { useConceptName } from "../../hooks/useConceptName";
37
+ import styles from "./question-modal.scss";
38
+
39
+ type EditQuestionModalProps = {
40
+ onModalChange: (showModal: boolean) => void;
41
+ onQuestionEdit: (question: Question) => void;
42
+ onSchemaChange: (schema: Schema) => void;
43
+ pageIndex: number;
44
+ questionIndex: number;
45
+ questionToEdit: Question;
46
+ resetIndices: () => void;
47
+ schema: Schema;
48
+ sectionIndex: number;
49
+ showModal: boolean;
50
+ };
51
+
52
+ const EditQuestionModal: React.FC<EditQuestionModalProps> = ({
53
+ questionToEdit,
54
+ schema,
55
+ onSchemaChange,
56
+ pageIndex,
57
+ sectionIndex,
58
+ questionIndex,
59
+ resetIndices,
60
+ showModal,
61
+ onModalChange,
62
+ onQuestionEdit,
63
+ }) => {
64
+ const { t } = useTranslation();
65
+ const { fieldTypes, questionTypes } = useConfig();
66
+ const [max, setMax] = useState("");
67
+ const [min, setMin] = useState("");
68
+ const [questionLabel, setQuestionLabel] = useState("");
69
+ const [questionType, setQuestionType] = useState("");
70
+ const [isQuestionRequired, setIsQuestionRequired] = useState(false);
71
+ const [fieldType, setFieldType] = useState<FieldTypes>(null);
72
+ const [questionId, setQuestionId] = useState("");
73
+ const [answers, setAnswers] = useState<Answer[]>([]);
74
+ const [selectedConcept, setSelectedConcept] = useState(null);
75
+ const [conceptMappings, setConceptMappings] = useState<ConceptMapping[]>([]);
76
+ const [rows, setRows] = useState(2);
77
+ const [conceptToLookup, setConceptToLookup] = useState("");
78
+ const [selectedAnswers, setSelectedAnswers] = useState([]);
79
+ const { concepts, isLoadingConcepts } = useConceptLookup(conceptToLookup);
80
+ const { conceptName, isLoadingConceptName } = useConceptName(
81
+ questionToEdit.questionOptions.concept
82
+ );
83
+
84
+ const handleConceptChange = (event) => {
85
+ setConceptToLookup(event.target.value);
86
+ };
87
+
88
+ const handleConceptSelect = (concept: Concept) => {
89
+ setConceptToLookup("");
90
+ setSelectedConcept(concept);
91
+ setAnswers(
92
+ concept?.answers?.map((answer) => ({
93
+ concept: answer?.uuid,
94
+ label: answer?.display,
95
+ }))
96
+ );
97
+ setConceptMappings(
98
+ concept?.mappings?.map((conceptMapping) => {
99
+ let data = conceptMapping.display.split(": ");
100
+ return { type: data[0], value: data[1] };
101
+ })
102
+ );
103
+ };
104
+
105
+ const questionIdExists = (idToTest) => {
106
+ if (questionToEdit?.id === idToTest) {
107
+ return false;
108
+ }
109
+
110
+ const nestedIds = schema?.pages?.map((page) => {
111
+ return page?.sections?.map((section) => {
112
+ return section?.questions?.map((question) => {
113
+ return question.id;
114
+ });
115
+ });
116
+ });
117
+
118
+ const questionIds = flattenDeep(nestedIds);
119
+
120
+ return questionIds.includes(idToTest);
121
+ };
122
+
123
+ const handleUpdateQuestion = () => {
124
+ updateQuestion(questionIndex);
125
+ onModalChange(false);
126
+ };
127
+
128
+ const updateQuestion = (questionIndex) => {
129
+ try {
130
+ const mappedAnswers = selectedAnswers?.map((answer) => ({
131
+ concept: answer.id,
132
+ label: answer.text,
133
+ }));
134
+
135
+ const data = {
136
+ label: questionLabel ? questionLabel : questionToEdit.label,
137
+ type: questionType ? questionType : questionToEdit.type,
138
+ required: isQuestionRequired
139
+ ? isQuestionRequired
140
+ : /true/.test(questionToEdit?.required),
141
+ id: questionId ? questionId : questionToEdit.id,
142
+ questionOptions: {
143
+ rendering: fieldType
144
+ ? fieldType
145
+ : questionToEdit.questionOptions.rendering,
146
+ concept: selectedConcept?.uuid
147
+ ? selectedConcept.uuid
148
+ : questionToEdit.questionOptions.concept,
149
+ conceptMappings: conceptMappings.length
150
+ ? conceptMappings
151
+ : questionToEdit.questionOptions.conceptMappings,
152
+ answers: mappedAnswers.length
153
+ ? mappedAnswers
154
+ : questionToEdit.questionOptions.answers,
155
+ },
156
+ };
157
+
158
+ schema.pages[pageIndex].sections[sectionIndex].questions[questionIndex] =
159
+ data;
160
+
161
+ onSchemaChange({ ...schema });
162
+
163
+ resetIndices();
164
+ setQuestionLabel("");
165
+ setQuestionId("");
166
+ setIsQuestionRequired(false);
167
+ setQuestionType(null);
168
+ setFieldType(null);
169
+ setSelectedConcept(null);
170
+ setConceptMappings([]);
171
+ setAnswers([]);
172
+ setSelectedAnswers([]);
173
+ onQuestionEdit(null);
174
+
175
+ showToast({
176
+ title: t("success", "Success!"),
177
+ kind: "success",
178
+ critical: true,
179
+ description: t("questionUpdated", "Question updated"),
180
+ });
181
+ } catch (error) {
182
+ showNotification({
183
+ title: t("errorUpdatingQuestion", "Error updating question"),
184
+ kind: "error",
185
+ critical: true,
186
+ description: error?.message,
187
+ });
188
+ }
189
+ };
190
+
191
+ return (
192
+ <ComposedModal open={showModal} onClose={() => onModalChange(false)}>
193
+ <ModalHeader title={t("editQuestion", "Edit question")} />
194
+ <Form onSubmit={(event) => event.preventDefault()}>
195
+ <ModalBody hasScrollingContent>
196
+ <FormGroup legendText={""}>
197
+ <Stack gap={5}>
198
+ <TextInput
199
+ defaultValue={questionToEdit.label}
200
+ id={questionToEdit.id}
201
+ labelText={t("questionLabel", "Label")}
202
+ onChange={(event) => setQuestionLabel(event.target.value)}
203
+ required
204
+ />
205
+ <RadioButtonGroup
206
+ defaultSelected={
207
+ /true/.test(questionToEdit?.required)
208
+ ? "required"
209
+ : "optional"
210
+ }
211
+ name="isQuestionRequired"
212
+ legendText={t(
213
+ "isQuestionRequiredOrOptional",
214
+ "Is this question a required or optional field? Required fields must be answered before the form can be submitted."
215
+ )}
216
+ >
217
+ <RadioButton
218
+ id="questionIsNotRequired"
219
+ defaultChecked={true}
220
+ labelText={t("optional", "Optional")}
221
+ onClick={() => setIsQuestionRequired(false)}
222
+ value="optional"
223
+ />
224
+ <RadioButton
225
+ id="questionIsRequired"
226
+ defaultChecked={false}
227
+ labelText={t("required", "Required")}
228
+ onClick={() => setIsQuestionRequired(true)}
229
+ value="required"
230
+ />
231
+ </RadioButtonGroup>
232
+ <Select
233
+ defaultValue={questionToEdit.type}
234
+ onChange={(event) => setQuestionType(event.target.value)}
235
+ id={"questionType"}
236
+ invalidText={t("typeRequired", "Type is required")}
237
+ labelText={t("questionType", "Question type")}
238
+ required
239
+ >
240
+ {!questionType && (
241
+ <SelectItem
242
+ text={t("chooseQuestionType", "Choose a question type")}
243
+ value=""
244
+ />
245
+ )}
246
+ {questionTypes.map((questionType, key) => (
247
+ <SelectItem
248
+ text={questionType}
249
+ value={questionType}
250
+ key={key}
251
+ />
252
+ ))}
253
+ </Select>
254
+ <Select
255
+ defaultValue={questionToEdit.questionOptions.rendering}
256
+ onChange={(event) => setFieldType(event.target.value)}
257
+ id="renderingType"
258
+ invalidText={t(
259
+ "validFieldTypeRequired",
260
+ "A valid field type value is required"
261
+ )}
262
+ labelText={t("fieldType", "Field type")}
263
+ required
264
+ >
265
+ {!fieldType && (
266
+ <SelectItem
267
+ text={t("chooseFieldType", "Choose a field type")}
268
+ value=""
269
+ />
270
+ )}
271
+ {fieldTypes.map((fieldType, key) => (
272
+ <SelectItem text={fieldType} value={fieldType} key={key} />
273
+ ))}
274
+ </Select>
275
+ {fieldType === FieldTypes.Number ? (
276
+ <>
277
+ <TextInput
278
+ id="min"
279
+ labelText="Min"
280
+ value={min || ""}
281
+ onChange={(event) => setMin(event.target.value)}
282
+ required
283
+ />
284
+ <TextInput
285
+ id="max"
286
+ labelText="Max"
287
+ value={max || ""}
288
+ onChange={(event) => setMax(event.target.value)}
289
+ required
290
+ />
291
+ </>
292
+ ) : fieldType === FieldTypes.TextArea ? (
293
+ <TextInput
294
+ id="textAreaRows"
295
+ labelText={t("rows", "Rows")}
296
+ value={rows || ""}
297
+ onChange={(event) => setRows(event.target.value)}
298
+ required
299
+ />
300
+ ) : null}
301
+ <TextInput
302
+ defaultValue={questionToEdit.id}
303
+ id="questionId"
304
+ invalid={questionIdExists(questionId)}
305
+ invalidText={t(
306
+ "questionIdExists",
307
+ "This question ID already exists in your schema"
308
+ )}
309
+ labelText={t(
310
+ "questionId",
311
+ "Question ID (prefer camel-case for IDs)"
312
+ )}
313
+ onChange={(event) => setQuestionId(event.target.value)}
314
+ placeholder={t(
315
+ "questionIdPlaceholder",
316
+ 'Enter a unique ID e.g. "anaesthesiaType" for a question asking about the type of anaesthesia.'
317
+ )}
318
+ required
319
+ />
320
+ {fieldType !== FieldTypes.UiSelectExtended && (
321
+ <div>
322
+ <FormLabel className={styles.label}>
323
+ {t(
324
+ "selectBackingConcept",
325
+ "Select a backing concept for this question"
326
+ )}
327
+ </FormLabel>
328
+ {isLoadingConceptName ? (
329
+ <InlineLoading
330
+ className={styles.loader}
331
+ description={t("loading", "Loading") + "..."}
332
+ />
333
+ ) : (
334
+ <>
335
+ <Search
336
+ defaultValue={conceptName}
337
+ id="conceptLookup"
338
+ labelText={t("enterConceptName", "Enter concept name")}
339
+ placeholder={t("searchConcept", "Search concept")}
340
+ onClear={() => setSelectedConcept(null)}
341
+ onChange={handleConceptChange}
342
+ onInputChange={(event) => setConceptToLookup(event)}
343
+ required
344
+ size="md"
345
+ value={selectedConcept?.display}
346
+ />
347
+ {(() => {
348
+ if (!conceptToLookup) return null;
349
+ if (isLoadingConcepts)
350
+ return (
351
+ <InlineLoading
352
+ className={styles.loader}
353
+ description={t("searching", "Searching") + "..."}
354
+ />
355
+ );
356
+ if (
357
+ concepts &&
358
+ concepts?.length &&
359
+ !isLoadingConcepts
360
+ ) {
361
+ return (
362
+ <ul className={styles.conceptList}>
363
+ {concepts?.map((concept, index) => (
364
+ <li
365
+ role="menuitem"
366
+ className={styles.concept}
367
+ key={index}
368
+ onClick={() => handleConceptSelect(concept)}
369
+ >
370
+ {concept.display}
371
+ </li>
372
+ ))}
373
+ </ul>
374
+ );
375
+ }
376
+ return (
377
+ <Layer>
378
+ <Tile className={styles.emptyResults}>
379
+ <span>
380
+ {t(
381
+ "noMatchingConcepts",
382
+ "No concepts were found that match"
383
+ )}{" "}
384
+ <strong>"{conceptToLookup}".</strong>
385
+ </span>
386
+ </Tile>
387
+ </Layer>
388
+ );
389
+ })()}
390
+ </>
391
+ )}
392
+ </div>
393
+ )}
394
+
395
+ {selectedAnswers.length ? (
396
+ <div>
397
+ {selectedAnswers.map((selectedAnswer) => (
398
+ <Tag
399
+ className={styles.tag}
400
+ key={selectedAnswer.id}
401
+ type={"blue"}
402
+ >
403
+ {selectedAnswer.text}
404
+ </Tag>
405
+ ))}
406
+ </div>
407
+ ) : (
408
+ <div>
409
+ {questionToEdit?.questionOptions?.answers?.map((answer) => (
410
+ <Tag
411
+ className={styles.tag}
412
+ key={answer?.concept}
413
+ type={"blue"}
414
+ >
415
+ {answer?.label}
416
+ </Tag>
417
+ ))}
418
+ </div>
419
+ )}
420
+
421
+ {questionToEdit?.questionOptions?.answers &&
422
+ questionToEdit?.questionOptions.answers?.length ? (
423
+ <MultiSelect
424
+ direction="top"
425
+ id="selectAnswers"
426
+ itemToString={(item) => item.text}
427
+ initialSelectedItems={questionToEdit?.questionOptions?.answers?.map(
428
+ (answer) => ({
429
+ id: answer.concept,
430
+ text: answer.label,
431
+ })
432
+ )}
433
+ items={questionToEdit?.questionOptions?.answers?.map(
434
+ (answer) => ({
435
+ id: answer.concept,
436
+ text: answer.label,
437
+ })
438
+ )}
439
+ onChange={({ selectedItems }) =>
440
+ setSelectedAnswers(selectedItems.sort())
441
+ }
442
+ size="md"
443
+ titleText={t(
444
+ "selectAnswersToDisplay",
445
+ "Select answers to display"
446
+ )}
447
+ />
448
+ ) : null}
449
+ </Stack>
450
+ </FormGroup>
451
+ </ModalBody>
452
+ <ModalFooter>
453
+ <Button onClick={() => onModalChange(false)} kind="secondary">
454
+ {t("cancel", "Cancel")}
455
+ </Button>
456
+ <Button onClick={handleUpdateQuestion}>
457
+ <span>{t("save", "Save")}</span>
458
+ </Button>
459
+ </ModalFooter>
460
+ </Form>
461
+ </ComposedModal>
462
+ );
463
+ };
464
+
465
+ export default EditQuestionModal;