@abgov/nx-adsp 5.8.0-beta.4 → 5.8.0-beta.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abgov/nx-adsp",
3
- "version": "5.8.0-beta.4",
3
+ "version": "5.8.0-beta.5",
4
4
  "license": "Apache-2.0",
5
5
  "main": "src/index.js",
6
6
  "description": "Government of Alberta - Nx plugin for ADSP apps.",
@@ -1,3 +1,13 @@
1
+ .form {
2
+ > fieldset {
3
+ display: none;
4
+ }
5
+
6
+ > fieldset.show {
7
+ display: block;
8
+ }
9
+ }
10
+
1
11
  .formActions {
2
12
  display: flex;
3
13
  margin-top: 16px;
@@ -5,8 +15,40 @@
5
15
  > * {
6
16
  margin-left: 16px;
7
17
  }
18
+
19
+ .save {
20
+ margin-right: auto;
21
+ display: flex;
22
+ opacity: 0;
23
+ transition-property: opacity;
24
+ transition-duration: 500ms;
25
+
26
+ > * {
27
+ margin-top: auto;
28
+ margin-bottom: auto;
29
+ margin-right: 8px;
30
+ }
31
+ }
8
32
 
33
+ .save[data-show='true'] {
34
+ opacity: 1;
35
+ }
36
+ }
37
+
38
+ .sectionActions {
39
+ display: flex;
40
+
9
41
  > *:first-child {
10
- margin-left: auto;
42
+ margin-right: auto;
43
+ }
44
+ }
45
+
46
+ .load {
47
+ display: flex;
48
+ flex-direction: row;
49
+ padding-top: 64px;
50
+ padding-bottom: 64px;
51
+ > * {
52
+ margin: auto;
11
53
  }
12
54
  }
@@ -0,0 +1,23 @@
1
+ import {
2
+ initial<%= className %>State,
3
+ <%= propertyName %>Actions,
4
+ <%= propertyName %>Reducer,
5
+ } from './<%= fileName %>.slice';
6
+
7
+ describe('<%= name %> slice', () => {
8
+ describe('<%= propertyName %>Reducer', () => {
9
+ it('can handle initial state', () => {
10
+ expect(<%= propertyName %>Reducer(undefined, { type: '' })).toEqual(
11
+ initial<%= className %>State
12
+ );
13
+ });
14
+
15
+ it('can handle set step action', () => {
16
+ expect(
17
+ <%= propertyName %>Reducer(undefined, <%= propertyName %>Actions.setStep(2))
18
+ ).toMatchObject({ step: 2 });
19
+ });
20
+
21
+ // TODO: Add state unit tests.
22
+ });
23
+ });
@@ -1,4 +1,5 @@
1
1
  import {
2
+ PayloadAction,
2
3
  createAction,
3
4
  createAsyncThunk,
4
5
  createSelector,
@@ -15,10 +16,10 @@ const FORM_SERVICE_URL = '<%= formServiceUrl %>';
15
16
  /*
16
17
  * Update these interfaces according to your requirements.
17
18
  */
18
- <%= interfaceDefinition %>
19
-
19
+ <%- interfaceDefinition %>
20
+ <% const sections = Object.entries(dataSchema.properties); %>
20
21
  const rules: Record<string, Record<string, RegExp>> = {
21
- <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
22
+ <%_ sections.forEach(function([sectionKey, section]) { _%>
22
23
  <%= sectionKey %>: {
23
24
  <%_ Object.entries(section.properties).filter(([_, { pattern }]) => !!pattern).forEach(function([key, value]) { _%>
24
25
  <%= key %>: new RegExp('<%- value.pattern %>')
@@ -28,17 +29,21 @@ const rules: Record<string, Record<string, RegExp>> = {
28
29
  };
29
30
 
30
31
  const required: Record<string, string[]> = {
31
- <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
32
+ <%_ sections.forEach(function([sectionKey, section]) { _%>
32
33
  <%= sectionKey %>: [<%- section.required?.map(req => `'${req}'`) || "" %>],
33
34
  <%_ }); _%>
34
35
  };
35
36
 
37
+ type FormStatus = 'draft' | 'locked' | 'submitted';
38
+ type SectionCompletion = 'complete' | 'incomplete' | null;
39
+
36
40
  export interface <%= className %>State {
37
41
  formId: string;
42
+ status: FormStatus;
43
+ step: number;
38
44
  values: <%= className %>;
39
45
  errors: Record<string, Record<string, boolean>>;
40
- complete: Record<string, boolean>;
41
- review: boolean;
46
+ complete: Record<string, SectionCompletion>;
42
47
  busy: {
43
48
  loading: boolean;
44
49
  saving: boolean;
@@ -46,6 +51,41 @@ export interface <%= className %>State {
46
51
  };
47
52
  }
48
53
 
54
+ function areSectionsComplete(
55
+ values: <%= className %>,
56
+ errors: Record<string, Record<string, boolean>> = {}
57
+ ): Record<string, SectionCompletion> {
58
+ const complete: Record<string, SectionCompletion> = {};
59
+ Object.entries(required).forEach(([section, requiredValues]) => {
60
+ const sectionValue = values[section] || {};
61
+
62
+ const hasValues = requiredValues
63
+ .map(
64
+ (required) =>
65
+ sectionValue[required] !== undefined &&
66
+ sectionValue[required] !== null &&
67
+ sectionValue[required] !== ''
68
+ )
69
+ .filter((hasValue) => hasValue);
70
+
71
+ const hasError =
72
+ Object.values(errors[section] || {}).filter((error) => error).length > 0;
73
+
74
+ // Note that sections with no required values are considered complete.
75
+ let completion: SectionCompletion = null;
76
+ if (!hasError && hasValues.length === requiredValues.length) {
77
+ completion = 'complete';
78
+ } else {
79
+ // This means partial completion.
80
+ completion = 'incomplete';
81
+ }
82
+
83
+ complete[section] = completion;
84
+ });
85
+
86
+ return complete;
87
+ }
88
+
49
89
  /**
50
90
  * Export an effect using createAsyncThunk from
51
91
  * the Redux Toolkit: https://redux-toolkit.js.org/api/createAsyncThunk
@@ -71,7 +111,7 @@ export const initializeForm = createAsyncThunk(
71
111
  throw new Error('No active user session.');
72
112
  }
73
113
 
74
- const { data: forms } = await axios.get<{ results: { id: string }[] }>(
114
+ const { data: forms } = await axios.get<{ results: { id: string; status: FormStatus }[] }>(
75
115
  `${FORM_SERVICE_URL}/form/v1/forms`,
76
116
  {
77
117
  headers: {
@@ -86,9 +126,9 @@ export const initializeForm = createAsyncThunk(
86
126
  }
87
127
  );
88
128
 
89
- let formId = forms.results?.[0]?.id;
90
- if (!formId) {
91
- const { data: form } = await axios.post<{ id: string }>(
129
+ let form = forms.results?.[0];
130
+ if (!form) {
131
+ const { data: newForm } = await axios.post<{ id: string; status: FormStatus }>(
92
132
  `${FORM_SERVICE_URL}/form/v1/forms`,
93
133
  {
94
134
  definitionId: FORM_DEFINITION_ID,
@@ -109,19 +149,21 @@ export const initializeForm = createAsyncThunk(
109
149
  },
110
150
  }
111
151
  );
112
- formId = form.id;
152
+ form = newForm;
113
153
  }
114
154
 
115
155
  const { data } = await axios.get<{
116
156
  id: string;
117
157
  data: <%= className %>;
118
- }>(`${FORM_SERVICE_URL}/form/v1/forms/${formId}/data`, {
158
+ }>(`${FORM_SERVICE_URL}/form/v1/forms/${form.id}/data`, {
119
159
  headers: {
120
160
  Authorization: `Bearer ${user.access_token}`,
121
161
  },
122
162
  });
123
163
 
124
- return data;
164
+ const complete = areSectionsComplete(data.data);
165
+
166
+ return { ...form, ...data, complete };
125
167
  }
126
168
  );
127
169
 
@@ -172,20 +214,7 @@ export const updateForm = createAsyncThunk(
172
214
  errors[section] = sectionErrors;
173
215
  });
174
216
 
175
- const complete: Record<string, boolean> = {};
176
- Object.entries(required).forEach(([section, requiredValues]) => {
177
- const sectionValue = values[section] || {};
178
- complete[section] =
179
- Object.values(errors[section]).filter((error) => error).length < 1 &&
180
- requiredValues
181
- .map(
182
- (required) =>
183
- sectionValue[required] !== undefined &&
184
- sectionValue[required] !== null &&
185
- sectionValue[required] !== ''
186
- )
187
- .filter((hasValue) => !hasValue).length < 1;
188
- });
217
+ const complete = areSectionsComplete(values, errors);
189
218
 
190
219
  if (!hasError) {
191
220
  dispatch(queueSaveForm(values));
@@ -197,11 +226,19 @@ export const updateForm = createAsyncThunk(
197
226
 
198
227
  export const submitForm = createAsyncThunk(
199
228
  '<%= propertyName %>/submit',
200
- async (formId: string) => {
229
+ async (_, { getState }) => {
230
+ const state = getState();
231
+ const { user }: UserState = state['user'];
232
+ const { formId }: <%= className %>State = state[<%= constantName %>_FEATURE_KEY];
233
+
201
234
  const { data } = await axios.post(
202
235
  `${FORM_SERVICE_URL}/form/v1/forms/${formId}`,
203
236
  { operation: 'submit' },
204
- { headers: {} }
237
+ {
238
+ headers: {
239
+ Authorization: `Bearer ${user.access_token}`,
240
+ },
241
+ }
205
242
  );
206
243
  return data;
207
244
  }
@@ -209,10 +246,11 @@ export const submitForm = createAsyncThunk(
209
246
 
210
247
  export const initial<%= className %>State: <%= className %>State = {
211
248
  formId: null,
249
+ status: null,
250
+ step: 1,
212
251
  values: {} as <%= className %>,
213
252
  errors: {},
214
253
  complete: {},
215
- review: false,
216
254
  busy: {
217
255
  loading: false,
218
256
  saving: false,
@@ -223,7 +261,11 @@ export const initial<%= className %>State: <%= className %>State = {
223
261
  export const <%= propertyName %>Slice = createSlice({
224
262
  name: <%= constantName %>_FEATURE_KEY,
225
263
  initialState: initial<%= className %>State,
226
- reducers: {},
264
+ reducers: {
265
+ setStep: (state, action: PayloadAction<number>) => {
266
+ state.step = action.payload;
267
+ },
268
+ },
227
269
  extraReducers: (builder) => {
228
270
  builder
229
271
  .addCase(initializeForm.pending, (state) => {
@@ -231,7 +273,9 @@ export const <%= propertyName %>Slice = createSlice({
231
273
  })
232
274
  .addCase(initializeForm.fulfilled, (state, action) => {
233
275
  state.formId = action.payload.id;
276
+ state.status = action.payload.status;
234
277
  state.values = action.payload.data;
278
+ state.complete = action.payload.complete;
235
279
  state.busy.loading = false;
236
280
  })
237
281
  .addCase(initializeForm.rejected, (state) => {
@@ -250,6 +294,16 @@ export const <%= propertyName %>Slice = createSlice({
250
294
  })
251
295
  .addCase(queueSaveForm.rejected, (state) => {
252
296
  state.busy.saving = false;
297
+ })
298
+ .addCase(submitForm.pending, (state) => {
299
+ state.busy.submitting = true;
300
+ })
301
+ .addCase(submitForm.fulfilled, (state) => {
302
+ state.busy.submitting = false;
303
+ state.status = 'submitted'
304
+ })
305
+ .addCase(submitForm.rejected, (state) => {
306
+ state.busy.submitting = false;
253
307
  });
254
308
  },
255
309
  });
@@ -299,6 +353,16 @@ export const get<%= className %>State = (
299
353
  rootState: unknown
300
354
  ): <%= className %>State => rootState[<%= constantName %>_FEATURE_KEY];
301
355
 
356
+ export const getFormStatus = createSelector(
357
+ get<%= className %>State,
358
+ (state) => state.status
359
+ );
360
+
361
+ export const getFormStep = createSelector(
362
+ get<%= className %>State,
363
+ (state) => state.step
364
+ );
365
+
302
366
  export const getFormValues = createSelector(
303
367
  get<%= className %>State,
304
368
  (state) => state.values
@@ -318,3 +382,17 @@ export const getFormComplete = createSelector(
318
382
  get<%= className %>State,
319
383
  (state) => state.complete
320
384
  );
385
+
386
+ export const getFormInReview = createSelector(
387
+ get<%= className %>State,
388
+ (state) => state.step === <%= sections.length + 1 %>
389
+ );
390
+
391
+ export const getFormCanSubmit = createSelector(
392
+ get<%= className %>State,
393
+ (state) =>
394
+ !state.busy.saving && !state.busy.submitting &&
395
+ state.status === 'draft' &&
396
+ Object.values(state.complete).filter((complete) => complete !== 'complete')
397
+ .length < 1
398
+ );
@@ -1,10 +1,18 @@
1
1
  import {
2
2
  GoAButton,
3
+ GoAButtonGroup,
4
+ GoACallout,
3
5
  GoACheckbox,
6
+ GoADropdown,
7
+ GoADropdownItem,
4
8
  GoAFormItem,
5
9
  GoAFormStep,
6
10
  GoAFormStepper,
7
11
  GoAInput,
12
+ GoAInputDate,
13
+ GoAInputDateTime,
14
+ GoAInputTime,
15
+ GoANotification,
8
16
  GoASpinner,
9
17
  } from '@abgov/react-components';
10
18
  import { FunctionComponent, useEffect } from 'react';
@@ -12,61 +20,134 @@ import { useDispatch, useSelector } from 'react-redux';
12
20
  import { AppDispatch } from '../../store';
13
21
  import {
14
22
  getFormBusy,
23
+ getFormCanSubmit,
15
24
  getFormComplete,
16
25
  getFormErrors,
26
+ getFormInReview,
27
+ getFormStatus,
28
+ getFormStep,
17
29
  getFormValues,
18
30
  initializeForm,
31
+ submitForm,
32
+ <%= propertyName %>Actions,
19
33
  updateForm,
20
34
  } from './<%= fileName %>.slice';
21
35
  import styles from './<%= fileName %>.module.css';
22
36
 
23
37
  interface FieldSetProps {
38
+ className?: string;
39
+ inReview: boolean;
40
+ isReadOnly: boolean;
24
41
  value: Record<string, unknown>;
25
42
  errors: Record<string, boolean>;
26
43
  onChange: (value: Record<string, unknown>) => void;
44
+ onEdit: () => void;
27
45
  }
28
46
 
29
47
  <% Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { %>
30
48
  const <%= section.className %>FieldSet: FunctionComponent<FieldSetProps> = ({
49
+ className,
50
+ inReview,
51
+ isReadOnly,
31
52
  value,
32
53
  errors,
33
54
  onChange,
55
+ onEdit,
34
56
  }) => {
35
57
  return (
36
- <fieldset>
58
+ <fieldset className={className}>
37
59
  <legend><%= section.title || sectionKey %></legend>
60
+ <div className={styles.sectionActions}>
61
+ <p><%= section.description %></p>
62
+ {!isReadOnly && inReview && <GoAButton type="tertiary" onClick={onEdit}>Edit</GoAButton>}
63
+ </div>
38
64
  <%_ Object.entries(section.properties).forEach(function([key, value]) { _%>
39
- <GoAFormItem label="<%= value.title || key %>" helpText="<%= value.description %>">
40
- <%_ switch(value.type) {
41
- case 'string': _%>
42
- <GoAInput
43
- type="text"
44
- error={errors['<%= key %>']}
45
- onChange={(name, updated) => onChange({ ...value, [name]: updated })}
46
- value={`${value.<%= key %> || ''}`}
47
- name="<%= key %>"
48
- />
49
- <%_ break;
50
- case 'number': _%>
51
- <GoAInput
52
- type="number"
53
- error={errors['<%= key %>']}
54
- onChange={(name, updated) => onChange({ ...value, [name]: updated })}
55
- value={`${value.<%= key %> || ''}`}
56
- name="<%= key %>"
57
- />
58
- <%_ break;
59
- case 'boolean': _%>
60
- <GoACheckbox
61
- checked={value.<%= key %> as any || false}
62
- onChange={(name, updated) => onChange({ ...value, [name]: updated })}
63
- name="<%= key %>"
64
- />
65
- <%_ break;
66
- default:
67
- break; _%>
68
- <%_ } _%>
69
- </GoAFormItem>
65
+ <%_ if (!value.type) { _%>
66
+ <%_ if (value.constant) { _%>
67
+ <p><%= value.constant %></p>
68
+ <%_ } _%>
69
+ <%_ } else { _%>
70
+ <GoAFormItem label="<%= value.title || key %>" helpText="<%= value.description %>">
71
+ <%_ switch(value.type) {
72
+ case 'string': _%>
73
+ <%_ if (value.enum) { _%>
74
+ <GoADropdown
75
+ name="<%= key %>"
76
+ disabled={isReadOnly || inReview}
77
+ onChange={(name, updated) => onChange({ ...value, [name]: updated })}
78
+ value={`${value.<%= key %> || ''}`}
79
+ >
80
+ <%_ value.enum.forEach((enumValue) => { _%>
81
+ <GoADropdownItem value="<%= enumValue %>" label="<%= enumValue %>" />
82
+ <%_ }); _%>
83
+ </GoADropdown>
84
+ <%_ } else { _%>
85
+ <%_ switch(value.format) {
86
+ case 'date-time': _%>
87
+ <GoAInputDateTime
88
+ name="<%= key %>"
89
+ disabled={isReadOnly || inReview}
90
+ value={value.<%= key %> ? new Date(value.<%= key %> as string) : null}
91
+ onChange={(name, updated) => onChange({ ...value, [name]: (updated as Date)?.toISOString() })}
92
+ />
93
+ <%_ break;
94
+ case 'date': _%>
95
+ <GoAInputDate
96
+ name="<%= key %>"
97
+ disabled={isReadOnly || inReview}
98
+ value={value.<%= key %> ? new Date(value.<%= key %> as string) : null}
99
+ onChange={(name, updated) => onChange({ ...value, [name]: (updated as Date)?.toISOString() })}
100
+ />
101
+ <%_ break;
102
+ case 'time': _%>
103
+ <GoAInputTime
104
+ name="<%= key %>"
105
+ step={1}
106
+ disabled={isReadOnly || inReview}
107
+ value={value.<%= key %> ? new Date(value.<%= key %> as string) : null}
108
+ onChange={(name, updated) => onChange({ ...value, [name]: (updated as Date)?.toISOString() })}
109
+ />
110
+ <%_ break;
111
+ default: _%>
112
+ <GoAInput
113
+ type="text"
114
+ disabled={isReadOnly || inReview}
115
+ error={errors['<%= key %>']}
116
+ placeholder="<%= value?.examples?.join(', ') || '' %>"
117
+ onChange={(name, updated) => onChange({ ...value, [name]: updated })}
118
+ value={`${value.<%= key %> || ''}`}
119
+ name="<%= key %>"
120
+ />
121
+ <%_ break;
122
+ } _%>
123
+ <%_ } _%>
124
+ <%_ break;
125
+ case 'number': _%>
126
+ <GoAInput
127
+ type="number"
128
+ disabled={isReadOnly || inReview}
129
+ error={errors['<%= key %>']}
130
+ placeholder="<%= value?.examples || []%>"
131
+ onChange={(name, updated) => onChange({ ...value, [name]: updated })}
132
+ value={`${value.<%= key %> || ''}`}
133
+ name="<%= key %>"
134
+ />
135
+ <%_ break;
136
+ case 'boolean': _%>
137
+ <GoACheckbox
138
+ disabled={isReadOnly || inReview}
139
+ checked={value.<%= key %> as any || false}
140
+ onChange={(name, updated) => onChange({ ...value, [name]: updated })}
141
+ name="<%= key %>"
142
+ />
143
+ <%_ break;
144
+ case 'array': _%>
145
+ <%_ break;
146
+ default:
147
+ break; _%>
148
+ <%_ } _%>
149
+ </GoAFormItem>
150
+ <%_ } _%>
70
151
  <%_ }); _%>
71
152
  </fieldset>
72
153
  );
@@ -80,18 +161,36 @@ export const <%= className %>Form: FunctionComponent = () => {
80
161
  dispatch(initializeForm());
81
162
  }, [dispatch, user]);
82
163
 
164
+ const formStatus = useSelector(getFormStatus);
165
+ const formStep = useSelector(getFormStep);
83
166
  const formData = useSelector(getFormValues);
84
167
  const formErrors = useSelector(getFormErrors);
85
168
  const formBusy = useSelector(getFormBusy);
86
169
  const formComplete = useSelector(getFormComplete);
87
-
170
+ const inReview = useSelector(getFormInReview);
171
+ const canSubmit = useSelector(getFormCanSubmit);
172
+
173
+ <% const sections = Object.entries(dataSchema.properties); %>
88
174
  return (
89
- <form>
90
- <GoAFormStepper testId="<%= fileName %>>">
91
- <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
175
+ <form className={styles.form}>
176
+ <GoANotification type="important">
177
+ This is a generated rapid prototype. Use it as a starting point to build the right thing for users.
178
+ </GoANotification>
179
+ {
180
+ formStatus === 'submitted' &&
181
+ <GoACallout type="success" heading="<%= name %> form submitted">
182
+ We received your <%= name %> form and it is being processed.
183
+ </GoACallout>
184
+ }
185
+ <GoAFormStepper
186
+ testId="<%= fileName %>"
187
+ step={formStep}
188
+ onChange={(step) => dispatch(<%= propertyName %>Actions.setStep(step))}
189
+ >
190
+ <%_ sections.forEach(function([sectionKey, section]) { _%>
92
191
  <GoAFormStep
93
192
  text="<%= section.title || sectionKey %>"
94
- status={formComplete['<%= sectionKey %>'] ? 'complete' : null} />
193
+ status={formComplete['<%= sectionKey %>']} />
95
194
  <%_ }); _%>
96
195
  <GoAFormStep text="Review" />
97
196
  </GoAFormStepper>
@@ -101,20 +200,48 @@ export const <%= className %>Form: FunctionComponent = () => {
101
200
  </div>
102
201
  ) : (
103
202
  <>
104
- <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
105
- <<%= section.className %>FieldSet
203
+ <%_ sections.forEach(function([sectionKey, section], idx) { _%>
204
+ <<%= section.className %>FieldSet
205
+ className={formStep === <%= idx + 1 %> || formStep === <%= sections.length + 1 %> ? styles.show : ''}
206
+ inReview={inReview}
207
+ isReadOnly={formStatus !== 'draft'}
106
208
  value={formData.<%= sectionKey %> as any || {}}
107
209
  errors={formErrors['<%= sectionKey %>'] || {}}
108
210
  onChange={(value) =>
109
211
  dispatch(updateForm({ ...formData, '<%= sectionKey %>': value as any }))
110
212
  }
213
+ onEdit={() => dispatch(<%= propertyName %>Actions.setStep(<%= idx + 1 %>))}
111
214
  />
112
215
  <%_ }); _%>
113
216
  </>
114
217
  )}
115
218
  <div className={styles.formActions}>
116
- <GoAButton type="secondary">Back</GoAButton>
117
- <GoAButton type="primary">Next</GoAButton>
219
+ <div className={styles.save} data-show={formBusy.saving}>
220
+ <GoASpinner size="medium" type="infinite" />
221
+ <span>Saving...</span>
222
+ </div>
223
+ <GoAButtonGroup alignment="end">
224
+ {formStep > 1 && (
225
+ <GoAButton
226
+ type="secondary"
227
+ onClick={() => dispatch(<%= propertyName %>Actions.setStep(formStep - 1))}
228
+ >
229
+ Back
230
+ </GoAButton>
231
+ )}
232
+ {formStep < <%= sections.length + 1 %> ? (
233
+ <GoAButton
234
+ type="primary"
235
+ onClick={() => dispatch(<%= propertyName %>Actions.setStep(formStep + 1))}
236
+ >
237
+ Next
238
+ </GoAButton>
239
+ ) : (
240
+ <GoAButton disabled={!canSubmit} type="primary" onClick={() => dispatch(submitForm())}>
241
+ Submit
242
+ </GoAButton>
243
+ )}
244
+ </GoAButtonGroup>
118
245
  </div>
119
246
  </form>
120
247
  );