@openmrs/esm-form-builder-app 3.5.2-pre.3735 → 3.5.2-pre.3740

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.
@@ -1,9 +1,7 @@
1
1
  import React, { useCallback, useEffect, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { useParams } from 'react-router-dom';
4
3
  import {
5
4
  Button,
6
- ComposedModal,
7
5
  Form,
8
6
  FormGroup,
9
7
  InlineLoading,
@@ -43,22 +41,21 @@ interface FormGroupData {
43
41
  interface SaveFormModalProps {
44
42
  form: FormGroupData;
45
43
  schema: Schema;
44
+ formUuid?: string;
45
+ closeModal: () => void;
46
+ onSavingChange?: (isSaving: boolean) => void;
46
47
  }
47
48
 
48
- const SaveFormModal: React.FC<SaveFormModalProps> = ({ form, schema }) => {
49
+ const SaveFormModal: React.FC<SaveFormModalProps> = ({ form, schema, formUuid, closeModal, onSavingChange }) => {
49
50
  const { t } = useTranslation();
50
51
  const { encounterTypes } = useEncounterTypes();
51
- const { formUuid } = useParams<{ formUuid: string }>();
52
52
  const { mutate } = useForm(formUuid);
53
- const isSavingNewForm = !formUuid;
54
53
  const [description, setDescription] = useState('');
55
54
  const [encounterType, setEncounterType] = useState('');
56
55
  const [isInvalidVersion, setIsInvalidVersion] = useState(false);
57
56
  const [isSavingForm, setIsSavingForm] = useState(false);
58
57
  const [name, setName] = useState('');
59
- const [openConfirmSaveModal, setOpenConfirmSaveModal] = useState(false);
60
- const [openSaveFormModal, setOpenSaveFormModal] = useState(false);
61
- const [saveState, setSaveState] = useState('');
58
+ const [saveState, setSaveState] = useState<'update' | 'newVersion' | null>(formUuid ? null : 'newVersion');
62
59
  const [version, setVersion] = useState('');
63
60
 
64
61
  const clearDraftFormSchema = useCallback(() => localStorage.removeItem('formJSON'), []);
@@ -78,285 +75,248 @@ const SaveFormModal: React.FC<SaveFormModalProps> = ({ form, schema }) => {
78
75
  setIsInvalidVersion(!/^[0-9]/.test(version));
79
76
  };
80
77
 
81
- const openModal = useCallback((option: string) => {
82
- if (option === 'newVersion') {
83
- setSaveState('newVersion');
84
- setOpenConfirmSaveModal(false);
85
- setOpenSaveFormModal(true);
86
- } else if (option === 'new') {
87
- setSaveState('newVersion');
88
- setOpenSaveFormModal(true);
89
- } else if (option === 'update') {
90
- setSaveState('update');
91
- setOpenConfirmSaveModal(false);
92
- setOpenSaveFormModal(true);
93
- }
94
- }, []);
95
-
96
78
  const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
97
79
  event.preventDefault();
98
80
  setIsSavingForm(true);
81
+ onSavingChange?.(true);
82
+ try {
83
+ if (saveState === 'newVersion') {
84
+ try {
85
+ const newForm = await saveNewForm(name, version, false, description, encounterType);
99
86
 
100
- const target = event.target as EventTarget & {
101
- name: { value: string };
102
- version: { value: string };
103
- encounterType: { value: string };
104
- description: { value: string };
105
- };
106
-
107
- if (saveState === 'new' || saveState === 'newVersion') {
108
- const name = target.name.value,
109
- version = target.version.value,
110
- encounterType = target.encounterType.value,
111
- description = target.description.value;
87
+ const updatedSchema = {
88
+ ...schema,
89
+ name: name,
90
+ version: version,
91
+ description: description,
92
+ encounterType: encounterType,
93
+ uuid: newForm.uuid,
94
+ };
112
95
 
113
- try {
114
- const newForm = await saveNewForm(name, version, false, description, encounterType);
96
+ let newValueReference: string | undefined;
97
+ try {
98
+ newValueReference = (await uploadSchema(updatedSchema)).toString();
99
+ await getResourceUuid(newForm.uuid, newValueReference);
100
+ } catch (error) {
101
+ // Clean up the orphaned clobdata and form since schema upload or linking failed
102
+ if (newValueReference) {
103
+ await deleteClobdata(newValueReference).catch(() => {});
104
+ }
105
+ await deleteForm(newForm.uuid).catch(() => {});
106
+ throw error;
107
+ }
115
108
 
116
- const updatedSchema = {
117
- ...schema,
118
- name: name,
119
- version: version,
120
- description: description,
121
- encounterType: encounterType,
122
- uuid: newForm.uuid,
123
- };
109
+ showSnackbar({
110
+ title: t('formCreated', 'New form created'),
111
+ kind: 'success',
112
+ isLowContrast: true,
113
+ subtitle:
114
+ name +
115
+ ' ' +
116
+ t('saveSuccessMessage', 'was created successfully. It is now visible on the Forms dashboard.'),
117
+ });
118
+ clearDraftFormSchema();
119
+ closeModal();
120
+ await mutate();
124
121
 
125
- let newValueReference: string | undefined;
126
- try {
127
- newValueReference = (await uploadSchema(updatedSchema)).toString();
128
- await getResourceUuid(newForm.uuid, newValueReference);
122
+ navigate({
123
+ to: `${window.spaBase}/form-builder/edit/${newForm.uuid}`,
124
+ });
129
125
  } catch (error) {
130
- // Clean up the orphaned clobdata and form since schema upload or linking failed
131
- if (newValueReference) {
132
- await deleteClobdata(newValueReference).catch(() => {});
126
+ if (error instanceof Error) {
127
+ showSnackbar({
128
+ title: t('errorCreatingForm', 'Error creating form'),
129
+ kind: 'error',
130
+ subtitle: error?.message,
131
+ });
133
132
  }
134
- await deleteForm(newForm.uuid).catch(() => {});
135
- throw error;
136
133
  }
134
+ } else {
135
+ try {
136
+ const updatedSchema = {
137
+ ...schema,
138
+ name: name,
139
+ version: version,
140
+ description: description,
141
+ encounterType: encounterType,
142
+ };
137
143
 
138
- showSnackbar({
139
- title: t('formCreated', 'New form created'),
140
- kind: 'success',
141
- isLowContrast: true,
142
- subtitle:
143
- name + ' ' + t('saveSuccessMessage', 'was created successfully. It is now visible on the Forms dashboard.'),
144
- });
145
- clearDraftFormSchema();
146
- setOpenSaveFormModal(false);
147
- await mutate();
144
+ await updateForm(form.uuid, name, version, description, encounterType);
148
145
 
149
- navigate({
150
- to: `${window.spaBase}/form-builder/edit/${newForm.uuid}`,
151
- });
146
+ const oldResource = form?.resources?.length
147
+ ? form.resources.find(({ name }) => name === 'JSON schema')
148
+ : undefined;
152
149
 
153
- setIsSavingForm(false);
154
- } catch (error) {
155
- if (error instanceof Error) {
156
- showSnackbar({
157
- title: t('errorCreatingForm', 'Error creating form'),
158
- kind: 'error',
159
- subtitle: error?.message,
160
- });
161
- }
162
- setIsSavingForm(false);
163
- }
164
- } else {
165
- try {
166
- const updatedSchema = {
167
- ...schema,
168
- name: name,
169
- version: version,
170
- description: description,
171
- encounterType: encounterType,
172
- };
150
+ // Upload the new clobdata first (doesn't affect the live form yet)
151
+ const newValueReference = (await uploadSchema(updatedSchema)).toString();
173
152
 
174
- await updateForm(form.uuid, name, version, description, encounterType);
175
-
176
- const oldResource = form?.resources?.length
177
- ? form.resources.find(({ name }) => name === 'JSON schema')
178
- : undefined;
179
-
180
- // Upload the new clobdata first (doesn't affect the live form yet)
181
- const newValueReference = (await uploadSchema(updatedSchema)).toString();
153
+ // Swap: remove old resource link, then link the new clobdata.
154
+ // If the swap fails, clean up the newly uploaded clobdata.
155
+ try {
156
+ if (oldResource) {
157
+ await deleteResource(form.uuid, oldResource.uuid);
158
+ }
159
+ await getResourceUuid(form.uuid, newValueReference);
160
+ } catch (error) {
161
+ await deleteClobdata(newValueReference).catch(() => {});
162
+ throw error;
163
+ }
182
164
 
183
- // Swap: remove old resource link, then link the new clobdata.
184
- // If the swap fails, clean up the newly uploaded clobdata.
185
- try {
165
+ // Clean up old clobdata. If this fails, the form still works
166
+ // (just an orphaned clobdata row in the database).
186
167
  if (oldResource) {
187
- await deleteResource(form.uuid, oldResource.uuid);
168
+ await deleteClobdata(oldResource.valueReference).catch(() => {});
188
169
  }
189
- await getResourceUuid(form.uuid, newValueReference);
190
- } catch (error) {
191
- await deleteClobdata(newValueReference).catch(() => {});
192
- throw error;
193
- }
194
-
195
- // Clean up old clobdata. If this fails, the form still works
196
- // (just an orphaned clobdata row in the database).
197
- if (oldResource) {
198
- await deleteClobdata(oldResource.valueReference).catch(() => {});
199
- }
200
170
 
201
- showSnackbar({
202
- title: t('success', 'Success!'),
203
- kind: 'success',
204
- isLowContrast: true,
205
- subtitle: form?.name + ' ' + t('saveSuccess', 'was updated successfully'),
206
- });
207
- setOpenSaveFormModal(false);
208
- await mutate();
209
- setIsSavingForm(false);
210
- } catch (error) {
211
- if (error instanceof Error) {
212
171
  showSnackbar({
213
- title: t('errorUpdatingForm', 'Error updating form'),
214
- kind: 'error',
215
- subtitle: error?.message,
172
+ title: t('success', 'Success!'),
173
+ kind: 'success',
174
+ isLowContrast: true,
175
+ subtitle: form?.name + ' ' + t('saveSuccess', 'was updated successfully'),
216
176
  });
177
+ closeModal();
178
+ await mutate();
179
+ } catch (error) {
180
+ if (error instanceof Error) {
181
+ showSnackbar({
182
+ title: t('errorUpdatingForm', 'Error updating form'),
183
+ kind: 'error',
184
+ subtitle: error?.message,
185
+ });
186
+ }
217
187
  }
218
-
219
- setIsSavingForm(false);
220
188
  }
189
+ } finally {
190
+ setIsSavingForm(false);
191
+ onSavingChange?.(false);
221
192
  }
222
193
  };
223
194
 
195
+ if (saveState === null) {
196
+ return (
197
+ <>
198
+ <ModalHeader closeModal={closeModal} title={t('saveConfirmation', 'Save or Update form')} />
199
+ <ModalBody>
200
+ <p>
201
+ {t(
202
+ 'saveAsModal',
203
+ "A version of the form you're working on already exists on the server. Do you want to update the form or to save it as a new version?",
204
+ )}
205
+ </p>
206
+ </ModalBody>
207
+ <ModalFooter>
208
+ <Button kind={'tertiary'} onClick={() => setSaveState('update')}>
209
+ {t('updateExistingForm', 'Update existing version')}
210
+ </Button>
211
+ <Button kind={'primary'} onClick={() => setSaveState('newVersion')}>
212
+ {t('saveAsNewForm', 'Save as a new form')}
213
+ </Button>
214
+ <Button kind={'secondary'} onClick={closeModal}>
215
+ {t('close', 'Close')}
216
+ </Button>
217
+ </ModalFooter>
218
+ </>
219
+ );
220
+ }
221
+
224
222
  return (
225
223
  <>
226
- {!isSavingNewForm ? (
227
- <ComposedModal
228
- open={openConfirmSaveModal}
229
- onClose={() => setOpenConfirmSaveModal(false)}
230
- preventCloseOnClickOutside
231
- >
232
- <ModalHeader title={t('saveConfirmation', 'Save or Update form')} />
233
- <ModalBody>
234
- <p>
235
- {t(
236
- 'saveAsModal',
237
- "A version of the form you're working on already exists on the server. Do you want to update the form or to save it as a new version?",
238
- )}
239
- </p>
240
- </ModalBody>
241
- <ModalFooter>
242
- <Button kind={'tertiary'} onClick={() => openModal('update')}>
243
- {t('updateExistingForm', 'Update existing version')}
244
- </Button>
245
- <Button kind={'primary'} onClick={() => openModal('newVersion')}>
246
- {t('saveAsNewForm', 'Save as a new form')}
247
- </Button>
248
- <Button kind={'secondary'} onClick={() => setOpenConfirmSaveModal(false)}>
249
- {t('close', 'Close')}
250
- </Button>
251
- </ModalFooter>
252
- </ComposedModal>
253
- ) : null}
254
-
255
- <ComposedModal open={openSaveFormModal} onClose={() => setOpenSaveFormModal(false)} preventCloseOnClickOutside>
256
- <ModalHeader title={t('saveFormToServer', 'Save form to server')} />
257
- <Form onSubmit={handleSubmit} className={styles.saveFormBody}>
258
- <ModalBody>
259
- <p>
260
- {t(
261
- 'saveExplainerText',
262
- 'Clicking the Save button saves your form schema to the database. To see your form in your frontend, you first need to publish it. Click the Publish button to publish your form.',
263
- )}
264
- </p>
265
- <FormGroup legendText={''}>
266
- <Stack gap={5}>
224
+ <ModalHeader closeModal={closeModal} title={t('saveFormToServer', 'Save form to server')} />
225
+ <ModalBody>
226
+ <Form id="save-form" onSubmit={handleSubmit}>
227
+ <p>
228
+ {t(
229
+ 'saveExplainerText',
230
+ 'Clicking the Save button saves your form schema to the database. To see your form in your frontend, you first need to publish it. Click the Publish button to publish your form.',
231
+ )}
232
+ </p>
233
+ <FormGroup legendText={''}>
234
+ <Stack gap={5}>
235
+ <TextInput
236
+ id="name"
237
+ labelText={t('formName', 'Form Name')}
238
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => setName(event.target.value)}
239
+ placeholder={t('formNamePlaceholder', 'e.g. OHRI Express Care Patient Encounter Form')}
240
+ required
241
+ value={name}
242
+ />
243
+ {saveState === 'update' ? (
267
244
  <TextInput
268
- id="name"
269
- labelText={t('formName', 'Form Name')}
270
- onChange={(event: React.ChangeEvent<HTMLInputElement>) => setName(event.target.value)}
271
- placeholder={t('formNamePlaceholder', 'e.g. OHRI Express Care Patient Encounter Form')}
272
- required
273
- value={name}
245
+ id="uuid"
246
+ labelText={t('autogeneratedUuid', 'UUID (auto-generated)')}
247
+ disabled
248
+ value={form?.uuid}
274
249
  />
275
- {saveState === 'update' ? (
276
- <TextInput
277
- id="uuid"
278
- labelText={t('autogeneratedUuid', 'UUID (auto-generated)')}
279
- disabled
280
- value={form?.uuid}
250
+ ) : null}
251
+ <TextInput
252
+ id="version"
253
+ labelText={t('version', 'Version')}
254
+ placeholder={t('versionPlaceholder', 'e.g. 1.0')}
255
+ onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
256
+ checkVersionValidity(event.target.value);
257
+
258
+ if (!isInvalidVersion) {
259
+ setVersion(event.target.value);
260
+ }
261
+ }}
262
+ invalid={isInvalidVersion}
263
+ invalidText={t('invalidVersionWarning', 'Version can only start with with a number')}
264
+ required
265
+ value={version}
266
+ />
267
+ <Select
268
+ id="encounterType"
269
+ labelText={t('encounterType', 'Encounter Type')}
270
+ onChange={(event: React.ChangeEvent<HTMLSelectElement>) => setEncounterType(event.target.value)}
271
+ required
272
+ value={encounterType}
273
+ >
274
+ {!encounterType ? (
275
+ <SelectItem
276
+ text={t('chooseEncounterType', 'Choose an encounter type to link your form to')}
277
+ value=""
281
278
  />
282
279
  ) : null}
283
- <TextInput
284
- id="version"
285
- labelText={t('version', 'Version')}
286
- placeholder={t('versionPlaceholder', 'e.g. 1.0')}
287
- onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
288
- checkVersionValidity(event.target.value);
289
-
290
- if (!isInvalidVersion) {
291
- setVersion(event.target.value);
292
- }
293
- }}
294
- invalid={isInvalidVersion}
295
- invalidText={t('invalidVersionWarning', 'Version can only start with with a number')}
296
- required
297
- value={version}
298
- />
299
- <Select
300
- id="encounterType"
301
- labelText={t('encounterType', 'Encounter Type')}
302
- onChange={(event: React.ChangeEvent<HTMLSelectElement>) => setEncounterType(event.target.value)}
303
- required
304
- value={encounterType}
305
- >
306
- {!encounterType ? (
307
- <SelectItem
308
- text={t('chooseEncounterType', 'Choose an encounter type to link your form to')}
309
- value=""
310
- />
311
- ) : null}
312
- {encounterTypes?.length > 0 &&
313
- encounterTypes.map((encounterType) => (
314
- <SelectItem key={encounterType.uuid} value={encounterType.uuid} text={encounterType.name}>
315
- {encounterType.name}
316
- </SelectItem>
317
- ))}
318
- </Select>
319
- <TextArea
320
- labelText={t('description', 'Description')}
321
- onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(event.target.value)}
322
- id="description"
323
- placeholder={t(
324
- 'descriptionPlaceholderText',
325
- 'e.g. A form used to collect encounter data for clients in the Express Care program.',
326
- )}
327
- required
328
- value={description}
329
- />
330
- </Stack>
331
- </FormGroup>
332
- </ModalBody>
333
- <ModalFooter>
334
- <Button kind={'secondary'} onClick={() => setOpenSaveFormModal(false)}>
335
- {t('close', 'Close')}
336
- </Button>
337
- <Button
338
- disabled={isSavingForm || isInvalidVersion}
339
- className={styles.spinner}
340
- type={'submit'}
341
- kind={'primary'}
342
- >
343
- {isSavingForm ? (
344
- <InlineLoading description={t('saving', 'Saving') + '...'} />
345
- ) : (
346
- <span>{t('save', 'Save')}</span>
347
- )}
348
- </Button>
349
- </ModalFooter>
280
+ {encounterTypes?.length > 0 &&
281
+ encounterTypes.map((encounterType) => (
282
+ <SelectItem key={encounterType.uuid} value={encounterType.uuid} text={encounterType.name}>
283
+ {encounterType.name}
284
+ </SelectItem>
285
+ ))}
286
+ </Select>
287
+ <TextArea
288
+ labelText={t('description', 'Description')}
289
+ onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => setDescription(event.target.value)}
290
+ id="description"
291
+ placeholder={t(
292
+ 'descriptionPlaceholderText',
293
+ 'e.g. A form used to collect encounter data for clients in the Express Care program.',
294
+ )}
295
+ required
296
+ value={description}
297
+ />
298
+ </Stack>
299
+ </FormGroup>
350
300
  </Form>
351
- </ComposedModal>
352
-
353
- <Button
354
- disabled={!schema}
355
- kind="primary"
356
- onClick={() => (isSavingNewForm ? openModal('new') : setOpenConfirmSaveModal(true))}
357
- >
358
- {t('saveForm', 'Save form')}
359
- </Button>
301
+ </ModalBody>
302
+ <ModalFooter>
303
+ <Button kind={'secondary'} onClick={closeModal}>
304
+ {t('close', 'Close')}
305
+ </Button>
306
+ <Button
307
+ disabled={isSavingForm || isInvalidVersion}
308
+ className={styles.spinner}
309
+ type={'submit'}
310
+ form="save-form"
311
+ kind={'primary'}
312
+ >
313
+ {isSavingForm ? (
314
+ <InlineLoading description={t('saving', 'Saving') + '...'} />
315
+ ) : (
316
+ <span>{t('save', 'Save')}</span>
317
+ )}
318
+ </Button>
319
+ </ModalFooter>
360
320
  </>
361
321
  );
362
322
  };
@@ -9,8 +9,3 @@
9
9
  font-size: unset;
10
10
  }
11
11
  }
12
-
13
- .saveFormBody {
14
- max-height: 75vh;
15
- overflow-y: auto;
16
- }