@abgov/nx-adsp 5.8.0-beta.4 → 5.8.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.
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",
4
4
  "license": "Apache-2.0",
5
5
  "main": "src/index.js",
6
6
  "description": "Government of Alberta - Nx plugin for ADSP apps.",
@@ -0,0 +1,23 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <title><%= projectName %></title>
6
+ <base href="/" />
7
+
8
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
9
+ <link rel="icon" type="image/x-icon" href="favicon.ico" />
10
+
11
+ <script
12
+ type="module"
13
+ src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.esm.js"
14
+ ></script>
15
+ <script
16
+ nomodule
17
+ src="https://unpkg.com/ionicons@5.5.2/dist/ionicons/ionicons.js"
18
+ ></script>
19
+ </head>
20
+ <body>
21
+ <div id="root"></div>
22
+ </body>
23
+ </html>
@@ -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,144 @@ 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 'integer':
126
+ case 'number': _%>
127
+ <GoAInput
128
+ type="number"
129
+ disabled={isReadOnly || inReview}
130
+ error={errors['<%= key %>']}
131
+ placeholder="<%= value?.examples || []%>"
132
+ onChange={(name, updated) => onChange({ ...value, [name]: updated })}
133
+ value={`${value.<%= key %> || ''}`}
134
+ name="<%= key %>"
135
+ <%_ if (value?.maximum !== undefined) { _%>
136
+ max={<%= value.maximum %>}
137
+ <%_ } _%>
138
+ <%_ if (value?.minimum !== undefined) { _%>
139
+ min={<%= value.minimum %>}
140
+ <%_ } _%>
141
+ <%_ if (value?.multipleOf !== undefined) { _%>
142
+ step={<%= value.multipleOf %>}
143
+ <%_ } _%>
144
+ />
145
+ <%_ break;
146
+ case 'boolean': _%>
147
+ <GoACheckbox
148
+ disabled={isReadOnly || inReview}
149
+ checked={value.<%= key %> as any || false}
150
+ onChange={(name, updated) => onChange({ ...value, [name]: updated })}
151
+ name="<%= key %>"
152
+ />
153
+ <%_ break;
154
+ case 'array': _%>
155
+ <%_ break;
156
+ default:
157
+ break; _%>
158
+ <%_ } _%>
159
+ </GoAFormItem>
160
+ <%_ } _%>
70
161
  <%_ }); _%>
71
162
  </fieldset>
72
163
  );
@@ -80,18 +171,36 @@ export const <%= className %>Form: FunctionComponent = () => {
80
171
  dispatch(initializeForm());
81
172
  }, [dispatch, user]);
82
173
 
174
+ const formStatus = useSelector(getFormStatus);
175
+ const formStep = useSelector(getFormStep);
83
176
  const formData = useSelector(getFormValues);
84
177
  const formErrors = useSelector(getFormErrors);
85
178
  const formBusy = useSelector(getFormBusy);
86
179
  const formComplete = useSelector(getFormComplete);
87
-
180
+ const inReview = useSelector(getFormInReview);
181
+ const canSubmit = useSelector(getFormCanSubmit);
182
+
183
+ <% const sections = Object.entries(dataSchema.properties); %>
88
184
  return (
89
- <form>
90
- <GoAFormStepper testId="<%= fileName %>>">
91
- <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
185
+ <form className={styles.form}>
186
+ <GoANotification type="important">
187
+ This is a generated rapid prototype. Use it as a starting point to build the right thing for users.
188
+ </GoANotification>
189
+ {
190
+ formStatus === 'submitted' &&
191
+ <GoACallout type="success" heading="<%= name %> form submitted">
192
+ We received your <%= name %> form and it is being processed.
193
+ </GoACallout>
194
+ }
195
+ <GoAFormStepper
196
+ testId="<%= fileName %>"
197
+ step={formStep}
198
+ onChange={(step) => dispatch(<%= propertyName %>Actions.setStep(step))}
199
+ >
200
+ <%_ sections.forEach(function([sectionKey, section]) { _%>
92
201
  <GoAFormStep
93
202
  text="<%= section.title || sectionKey %>"
94
- status={formComplete['<%= sectionKey %>'] ? 'complete' : null} />
203
+ status={formComplete['<%= sectionKey %>']} />
95
204
  <%_ }); _%>
96
205
  <GoAFormStep text="Review" />
97
206
  </GoAFormStepper>
@@ -101,20 +210,48 @@ export const <%= className %>Form: FunctionComponent = () => {
101
210
  </div>
102
211
  ) : (
103
212
  <>
104
- <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
105
- <<%= section.className %>FieldSet
213
+ <%_ sections.forEach(function([sectionKey, section], idx) { _%>
214
+ <<%= section.className %>FieldSet
215
+ className={formStep === <%= idx + 1 %> || formStep === <%= sections.length + 1 %> ? styles.show : ''}
216
+ inReview={inReview}
217
+ isReadOnly={formStatus !== 'draft'}
106
218
  value={formData.<%= sectionKey %> as any || {}}
107
219
  errors={formErrors['<%= sectionKey %>'] || {}}
108
220
  onChange={(value) =>
109
221
  dispatch(updateForm({ ...formData, '<%= sectionKey %>': value as any }))
110
222
  }
223
+ onEdit={() => dispatch(<%= propertyName %>Actions.setStep(<%= idx + 1 %>))}
111
224
  />
112
225
  <%_ }); _%>
113
226
  </>
114
227
  )}
115
228
  <div className={styles.formActions}>
116
- <GoAButton type="secondary">Back</GoAButton>
117
- <GoAButton type="primary">Next</GoAButton>
229
+ <div className={styles.save} data-show={formBusy.saving}>
230
+ <GoASpinner size="medium" type="infinite" />
231
+ <span>Saving...</span>
232
+ </div>
233
+ <GoAButtonGroup alignment="end">
234
+ {formStep > 1 && (
235
+ <GoAButton
236
+ type="secondary"
237
+ onClick={() => dispatch(<%= propertyName %>Actions.setStep(formStep - 1))}
238
+ >
239
+ Back
240
+ </GoAButton>
241
+ )}
242
+ {formStep < <%= sections.length + 1 %> ? (
243
+ <GoAButton
244
+ type="primary"
245
+ onClick={() => dispatch(<%= propertyName %>Actions.setStep(formStep + 1))}
246
+ >
247
+ Next
248
+ </GoAButton>
249
+ ) : (
250
+ <GoAButton disabled={!canSubmit} type="primary" onClick={() => dispatch(submitForm())}>
251
+ Submit
252
+ </GoAButton>
253
+ )}
254
+ </GoAButtonGroup>
118
255
  </div>
119
256
  </form>
120
257
  );