@openmrs/esm-form-builder-app 3.5.2-pre.3588 → 3.5.2-pre.3594

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 (91) hide show
  1. package/dist/1480.js +1 -1
  2. package/dist/1480.js.map +1 -1
  3. package/dist/1646.js +1 -1
  4. package/dist/1646.js.map +1 -1
  5. package/dist/1869.js +1 -1
  6. package/dist/1869.js.map +1 -1
  7. package/dist/1877.js +1 -1
  8. package/dist/1877.js.map +1 -1
  9. package/dist/2317.js +1 -1
  10. package/dist/2317.js.map +1 -1
  11. package/dist/2416.js +1 -1
  12. package/dist/2416.js.map +1 -1
  13. package/dist/282.js +1 -1
  14. package/dist/282.js.map +1 -1
  15. package/dist/2881.js +1 -1
  16. package/dist/2881.js.map +1 -1
  17. package/dist/3963.js +1 -1
  18. package/dist/3963.js.map +1 -1
  19. package/dist/4106.js +1 -1
  20. package/dist/4106.js.map +1 -1
  21. package/dist/4111.js +1 -1
  22. package/dist/4111.js.map +1 -1
  23. package/dist/434.js +1 -1
  24. package/dist/434.js.map +1 -1
  25. package/dist/4348.js +1 -1
  26. package/dist/4348.js.map +1 -1
  27. package/dist/4658.js +1 -1
  28. package/dist/4658.js.map +1 -1
  29. package/dist/4928.js +1 -1
  30. package/dist/4928.js.map +1 -1
  31. package/dist/5117.js +1 -1
  32. package/dist/5117.js.map +1 -1
  33. package/dist/5132.js +1 -1
  34. package/dist/5132.js.map +1 -1
  35. package/dist/5145.js +1 -1
  36. package/dist/5145.js.map +1 -1
  37. package/dist/5503.js +1 -1
  38. package/dist/5503.js.map +1 -1
  39. package/dist/556.js +1 -1
  40. package/dist/556.js.map +1 -1
  41. package/dist/5644.js +1 -1
  42. package/dist/5644.js.map +1 -1
  43. package/dist/5940.js +1 -1
  44. package/dist/5940.js.map +1 -1
  45. package/dist/6047.js +1 -1
  46. package/dist/6047.js.map +1 -1
  47. package/dist/6371.js +1 -1
  48. package/dist/6371.js.map +1 -1
  49. package/dist/6377.js +1 -1
  50. package/dist/6377.js.map +1 -1
  51. package/dist/6444.js +1 -1
  52. package/dist/6444.js.map +1 -1
  53. package/dist/6508.js +1 -1
  54. package/dist/6508.js.map +1 -1
  55. package/dist/6724.js +1 -1
  56. package/dist/6724.js.map +1 -1
  57. package/dist/6764.js +1 -1
  58. package/dist/6764.js.map +1 -1
  59. package/dist/6904.js +1 -1
  60. package/dist/6904.js.map +1 -1
  61. package/dist/7045.js +1 -1
  62. package/dist/7045.js.map +1 -1
  63. package/dist/7175.js +1 -1
  64. package/dist/7175.js.map +1 -1
  65. package/dist/7182.js +1 -1
  66. package/dist/7182.js.map +1 -1
  67. package/dist/7742.js +1 -1
  68. package/dist/7742.js.map +1 -1
  69. package/dist/7912.js +1 -1
  70. package/dist/7912.js.map +1 -1
  71. package/dist/8358.js +1 -1
  72. package/dist/8358.js.map +1 -1
  73. package/dist/8359.js +1 -1
  74. package/dist/8359.js.map +1 -1
  75. package/dist/8695.js +1 -1
  76. package/dist/8695.js.map +1 -1
  77. package/dist/903.js +1 -1
  78. package/dist/903.js.map +1 -1
  79. package/dist/9072.js +1 -1
  80. package/dist/9072.js.map +1 -1
  81. package/dist/9103.js +1 -1
  82. package/dist/9103.js.map +1 -1
  83. package/dist/9806.js +1 -1
  84. package/dist/9806.js.map +1 -1
  85. package/dist/997.js +1 -1
  86. package/dist/997.js.map +1 -1
  87. package/dist/openmrs-esm-form-builder-app.js.buildmanifest.json +129 -129
  88. package/dist/routes.json +1 -1
  89. package/package.json +1 -1
  90. package/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.component.tsx +60 -13
  91. package/src/components/interactive-builder/modals/question/question-form/rendering-types/inputs/select/select-answers.test.tsx +43 -13
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":">=2.2.0"},"pages":[{"component":"root","route":"form-builder","online":true,"offline":true,"order":1}],"extensions":[{"name":"system-administration-form-builder-card-link","slot":"system-admin-page-card-link-slot","component":"systemAdministrationFormBuilderCardLink","online":true,"offline":true}],"modals":[{"name":"new-form-modal","component":"newFormModal"},{"name":"new-page-modal","component":"newPageModal"},{"name":"delete-page-modal","component":"deletePageModal"},{"name":"new-section-modal","component":"newSectionModal"},{"name":"delete-section-modal","component":"deleteSectionModal"},{"name":"add-form-reference-modal","component":"addFormReferenceModal"},{"name":"question-modal","component":"questionModal"},{"name":"delete-question-modal","component":"deleteQuestionModal"},{"name":"edit-question-modal","component":"editQuestionModal"},{"name":"restore-draft-schema-modal","component":"restoreDraftSchemaModal"},{"name":"unpublish-form-modal","component":"unpublishFormModal"},{"name":"delete-form-modal","component":"deleteFormModal"},{"name":"edit-translation-modal","component":"editTranslationModal"}],"version":"3.5.2-pre.3588"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"fhir2":">=1.2","webservices.rest":">=2.2.0"},"pages":[{"component":"root","route":"form-builder","online":true,"offline":true,"order":1}],"extensions":[{"name":"system-administration-form-builder-card-link","slot":"system-admin-page-card-link-slot","component":"systemAdministrationFormBuilderCardLink","online":true,"offline":true}],"modals":[{"name":"new-form-modal","component":"newFormModal"},{"name":"new-page-modal","component":"newPageModal"},{"name":"delete-page-modal","component":"deletePageModal"},{"name":"new-section-modal","component":"newSectionModal"},{"name":"delete-section-modal","component":"deleteSectionModal"},{"name":"add-form-reference-modal","component":"addFormReferenceModal"},{"name":"question-modal","component":"questionModal"},{"name":"delete-question-modal","component":"deleteQuestionModal"},{"name":"edit-question-modal","component":"editQuestionModal"},{"name":"restore-draft-schema-modal","component":"restoreDraftSchemaModal"},{"name":"unpublish-form-modal","component":"unpublishFormModal"},{"name":"delete-form-modal","component":"deleteFormModal"},{"name":"edit-translation-modal","component":"editTranslationModal"}],"version":"3.5.2-pre.3594"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openmrs/esm-form-builder-app",
3
- "version": "3.5.2-pre.3588",
3
+ "version": "3.5.2-pre.3594",
4
4
  "license": "MPL-2.0",
5
5
  "description": "Form Builder for O3",
6
6
  "browser": "dist/openmrs-esm-form-builder-app.js",
@@ -1,4 +1,4 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
1
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import { InlineNotification, MultiSelect, Stack } from '@carbon/react';
3
3
  import { DndContext, type DragEndEvent } from '@dnd-kit/core';
4
4
  import { arrayMove, SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';
@@ -20,13 +20,40 @@ const SelectAnswers: React.FC = () => {
20
20
  const { formField, concept, setFormField } = useFormField();
21
21
  const [addedAnswers, setAddedAnswers] = useState<AnswerItem[]>([]);
22
22
  const [invalidAnswerIds, setInvalidAnswerIds] = useState<string[]>([]);
23
+ const initializedConceptUuid = useRef<string | null>(null);
23
24
 
25
+ // Reset the additionally selected answers when the concept changes
24
26
  useEffect(() => {
25
27
  if (!concept) {
26
28
  setAddedAnswers([]);
27
29
  }
28
30
  }, [concept]);
29
31
 
32
+ // Initialize the selected answers to the concept's answers, once per concept.
33
+ // The once-per-concept guard keeps a cleared selection cleared instead of
34
+ // re-selecting every answer whenever the answers list becomes empty.
35
+ useEffect(() => {
36
+ if (!concept || initializedConceptUuid.current === concept.uuid) {
37
+ return;
38
+ }
39
+ initializedConceptUuid.current = concept.uuid;
40
+
41
+ if (concept.answers?.length && !formField.questionOptions?.answers?.length) {
42
+ const initialAnswers = concept.answers.map((answer) => ({
43
+ concept: answer.uuid,
44
+ label: answer.display,
45
+ }));
46
+
47
+ setFormField((prevField) => ({
48
+ ...prevField,
49
+ questionOptions: {
50
+ ...prevField.questionOptions,
51
+ answers: initialAnswers,
52
+ },
53
+ }));
54
+ }
55
+ }, [concept, formField.questionOptions?.answers, setFormField]);
56
+
30
57
  const selectedAnswers = useMemo(() => {
31
58
  const answers =
32
59
  formField.questionOptions?.answers?.map((answer) => ({
@@ -39,10 +66,12 @@ const SelectAnswers: React.FC = () => {
39
66
 
40
67
  const handleSelectAnswers = useCallback(
41
68
  ({ selectedItems }: { selectedItems: Array<AnswerItem> }) => {
42
- const mappedAnswers = selectedItems.map((answer) => ({
43
- concept: answer.id,
44
- label: answer.text,
45
- }));
69
+ const mappedAnswers = selectedItems
70
+ .filter((answer) => answer.id !== 'select-all')
71
+ .map((answer) => ({
72
+ concept: answer.id,
73
+ label: answer.text,
74
+ }));
46
75
 
47
76
  setFormField((prevField) => {
48
77
  const currentAnswers = prevField.questionOptions?.answers || [];
@@ -113,10 +142,17 @@ const SelectAnswers: React.FC = () => {
113
142
 
114
143
  // If no answers from concept but we have form field answers, use those
115
144
  if (conceptAnswerItems.length === 0 && formFieldAnswers.length > 0) {
116
- return formFieldAnswers.map((answer) => ({
117
- id: answer.concept,
118
- text: answer.label,
119
- }));
145
+ return [
146
+ ...formFieldAnswers.map((answer) => ({
147
+ id: answer.concept,
148
+ text: answer.label,
149
+ })),
150
+ {
151
+ id: 'select-all',
152
+ text: t('selectAllOptions', 'Select all options'),
153
+ isSelectAll: true,
154
+ },
155
+ ];
120
156
  }
121
157
 
122
158
  const formFieldAnswerLabelsMap = new Map(formFieldAnswers.map((answer) => [answer.concept, answer.label]));
@@ -133,8 +169,16 @@ const SelectAnswers: React.FC = () => {
133
169
  text: answer.label,
134
170
  }));
135
171
 
136
- return [...answersFromConceptWithLabelsFromFormField, ...additionalAnswers];
137
- }, [concept?.answers, formField.questionOptions?.answers]);
172
+ return [
173
+ ...answersFromConceptWithLabelsFromFormField,
174
+ ...additionalAnswers,
175
+ {
176
+ id: 'select-all',
177
+ text: t('selectAllOptions', 'Select all options'),
178
+ isSelectAll: true,
179
+ },
180
+ ];
181
+ }, [concept?.answers, formField.questionOptions?.answers, t]);
138
182
 
139
183
  const validateAnswers = useCallback(async () => {
140
184
  if (!answerItems.length) {
@@ -144,7 +188,10 @@ const SelectAnswers: React.FC = () => {
144
188
 
145
189
  const originalAnswerIds = new Set((concept?.answers || []).map((ans) => ans.uuid));
146
190
  const invalidIds: string[] = [];
147
- const uniqueAnswers = Array.from(new Map(answerItems.map((a) => [a.id, a])).values());
191
+ // Filter out the 'select-all' option before validation
192
+ const uniqueAnswers = Array.from(
193
+ new Map(answerItems.filter((a) => a.id !== 'select-all').map((a) => [a.id, a])).values(),
194
+ );
148
195
 
149
196
  for (const answer of uniqueAnswers) {
150
197
  if (!originalAnswerIds.has(answer.id)) {
@@ -219,7 +266,7 @@ const SelectAnswers: React.FC = () => {
219
266
 
220
267
  return (
221
268
  <Stack gap={5}>
222
- {answerItems.length > 0 && (
269
+ {answerItems.length > 1 && (
223
270
  <MultiSelect
224
271
  className={styles.multiSelect}
225
272
  direction="bottom"
@@ -1,6 +1,6 @@
1
1
  import React from 'react';
2
2
  import { vi, describe, it, expect, beforeEach } from 'vitest';
3
- import { render, screen, within } from '@testing-library/react';
3
+ import { render, screen } from '@testing-library/react';
4
4
  import SelectAnswers from './select-answers.component';
5
5
  import { useConceptId } from '@hooks/useConceptId';
6
6
  import { useConceptLookup } from '@hooks/useConceptLookup';
@@ -69,7 +69,7 @@ describe('Select answers component', () => {
69
69
  ).toBeInTheDocument();
70
70
  });
71
71
 
72
- it('lets user select answers provided by concept', async () => {
72
+ it('shows answers provided by concept as selected initially', async () => {
73
73
  const user = userEvent.setup();
74
74
  renderComponent();
75
75
  const answersMenu = screen.getByRole('combobox', {
@@ -78,20 +78,50 @@ describe('Select answers component', () => {
78
78
  expect(answersMenu).toBeInTheDocument();
79
79
 
80
80
  await user.click(answersMenu);
81
- const answerOption1 = screen.getByRole('option', { name: /answer 1/i });
82
- expect(answerOption1).toBeInTheDocument();
83
- expect(screen.getByText(/answer 2/i)).toBeInTheDocument();
84
- await user.click(answerOption1);
85
- const option = screen.getByRole('option', {
81
+ const answerOption1 = screen.getByRole('checkbox', {
86
82
  name: /answer 1/i,
87
83
  });
88
- expect(within(option).getByText(/answer 1/i)).toBeInTheDocument();
84
+ const answerOption2 = screen.getByRole('checkbox', {
85
+ name: /answer 2/i,
86
+ });
87
+ expect(answerOption1).toBeChecked();
88
+ expect(answerOption2).toBeChecked();
89
+ });
89
90
 
90
- expect(
91
- screen.getByRole('combobox', {
92
- name: /select answers to display/i,
93
- }),
94
- ).toBeInTheDocument();
91
+ it('keeps a cleared selection cleared instead of re-selecting every answer', async () => {
92
+ const user = userEvent.setup();
93
+ renderComponent();
94
+ const answersMenu = screen.getByRole('combobox', {
95
+ name: /select answers to display/i,
96
+ });
97
+
98
+ await user.click(answersMenu);
99
+ await user.click(screen.getByRole('checkbox', { name: /answer 1/i }));
100
+ await user.click(screen.getByRole('checkbox', { name: /answer 2/i }));
101
+
102
+ expect(screen.getByRole('checkbox', { name: /answer 1/i })).not.toBeChecked();
103
+ expect(screen.getByRole('checkbox', { name: /answer 2/i })).not.toBeChecked();
104
+ });
105
+
106
+ it('renders the answers menu when a saved custom answer exists and the concept has no answers', () => {
107
+ render(
108
+ <FormFieldProvider
109
+ initialFormField={{
110
+ ...formField,
111
+ questionOptions: {
112
+ rendering: 'select',
113
+ answers: [{ concept: '999', label: 'Custom answer' }],
114
+ },
115
+ }}
116
+ selectedConcept={{ ...concept, answers: [] }}>
117
+ <SelectAnswers />
118
+ </FormFieldProvider>,
119
+ );
120
+
121
+ const answersMenu = screen.getByRole('combobox', {
122
+ name: /select answers to display/i,
123
+ });
124
+ expect(answersMenu).toBeInTheDocument();
95
125
  });
96
126
 
97
127
  it('lets users add additional answers if concept is of datatype coded', async () => {