@abgov/nx-adsp 5.7.0 → 5.8.0-beta.1

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/generators.json CHANGED
@@ -29,6 +29,11 @@
29
29
  "schema": "./src/generators/react-dotnet/schema.json",
30
30
  "description": "Generator that creates a React-Dotnet fullstack solution."
31
31
  },
32
+ "react-form": {
33
+ "factory": "./src/generators/react-form/react-form",
34
+ "schema": "./src/generators/react-form/schema.json",
35
+ "description": "Generator that creates a React component based on an ADSP Form Definition."
36
+ },
32
37
  "angular-app": {
33
38
  "factory": "./src/generators/angular-app/angular-app",
34
39
  "schema": "./src/generators/angular-app/schema.json",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abgov/nx-adsp",
3
- "version": "5.7.0",
3
+ "version": "5.8.0-beta.1",
4
4
  "license": "Apache-2.0",
5
5
  "main": "src/index.js",
6
6
  "description": "Government of Alberta - Nx plugin for ADSP apps.",
@@ -18,12 +18,13 @@
18
18
  "jest-mock": "28.1.3"
19
19
  },
20
20
  "dependencies": {
21
- "@abgov/nx-oc": "^5.2.0",
21
+ "@abgov/nx-oc": "^5.5.0",
22
22
  "@angular-devkit/core": "^15.0.0",
23
23
  "@angular-devkit/schematics": "^15.0.0",
24
24
  "axios": "^0.27.0",
25
25
  "enquirer": "^2.3.6",
26
26
  "express": "^4.18.1",
27
+ "json-schema-to-typescript": "^13.0.1",
27
28
  "open": "^8.4.0",
28
29
  "simple-oauth2": "^5.0.0"
29
30
  },
@@ -0,0 +1,12 @@
1
+ .formActions {
2
+ display: flex;
3
+ margin-top: 16px;
4
+
5
+ > * {
6
+ margin-left: 16px;
7
+ }
8
+
9
+ > *:first-child {
10
+ margin-left: auto;
11
+ }
12
+ }
@@ -0,0 +1,320 @@
1
+ import {
2
+ createAction,
3
+ createAsyncThunk,
4
+ createSelector,
5
+ createSlice,
6
+ } from '@reduxjs/toolkit';
7
+ import axios from 'axios';
8
+ import { debounce } from 'lodash';
9
+ import { UserState } from 'redux-oidc';
10
+
11
+ export const <%= constantName %>_FEATURE_KEY = '<%= fileName %>';
12
+ const FORM_DEFINITION_ID = '<%= fileName %>';
13
+ const FORM_SERVICE_URL = '<%= formServiceUrl %>';
14
+
15
+ /*
16
+ * Update these interfaces according to your requirements.
17
+ */
18
+ <%= interfaceDefinition %>
19
+
20
+ const rules: Record<string, Record<string, RegExp>> = {
21
+ <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
22
+ <%= sectionKey %>: {
23
+ <%_ Object.entries(section.properties).filter(([_, { pattern }]) => !!pattern).forEach(function([key, value]) { _%>
24
+ <%= key %>: new RegExp('<%- value.pattern %>')
25
+ <%_ }); _%>
26
+ },
27
+ <%_ }); _%>
28
+ };
29
+
30
+ const required: Record<string, string[]> = {
31
+ <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
32
+ <%= sectionKey %>: [<%- section.required?.map(req => `'${req}'`) || "" %>],
33
+ <%_ }); _%>
34
+ };
35
+
36
+ export interface <%= className %>State {
37
+ formId: string;
38
+ values: <%= className %>;
39
+ errors: Record<string, Record<string, boolean>>;
40
+ complete: Record<string, boolean>;
41
+ review: boolean;
42
+ busy: {
43
+ loading: boolean;
44
+ saving: boolean;
45
+ submitting: boolean;
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Export an effect using createAsyncThunk from
51
+ * the Redux Toolkit: https://redux-toolkit.js.org/api/createAsyncThunk
52
+ *
53
+ * e.g.
54
+ * ```
55
+ * import React, { useEffect } from 'react';
56
+ * import { useDispatch } from 'react-redux';
57
+ *
58
+ * // ...
59
+ *
60
+ * const dispatch = useDispatch();
61
+ * useEffect(() => {
62
+ * dispatch(fetchIntake())
63
+ * }, [dispatch]);
64
+ * ```
65
+ */
66
+ export const initializeForm = createAsyncThunk(
67
+ '<%= propertyName %>/initialize',
68
+ async (_, { getState }) => {
69
+ const { user }: UserState = getState()['user'];
70
+ if (!user) {
71
+ throw new Error('No active user session.');
72
+ }
73
+
74
+ const { data: forms } = await axios.get<{ results: { id: string }[] }>(
75
+ `${FORM_SERVICE_URL}/form/v1/forms`,
76
+ {
77
+ headers: {
78
+ Authorization: `Bearer ${user.access_token}`,
79
+ },
80
+ params: {
81
+ criteria: {
82
+ createdByIdEquals: user.profile.sub,
83
+ definitionIdEquals: FORM_DEFINITION_ID,
84
+ },
85
+ },
86
+ }
87
+ );
88
+
89
+ let formId = forms.results?.[0]?.id;
90
+ if (!formId) {
91
+ const { data: form } = await axios.post<{ id: string }>(
92
+ `${FORM_SERVICE_URL}/form/v1/forms`,
93
+ {
94
+ definitionId: FORM_DEFINITION_ID,
95
+ applicant: {
96
+ userId: user.profile.sub,
97
+ addressAs: user.profile.name,
98
+ channels: [
99
+ {
100
+ channel: 'email',
101
+ address: user.profile.email,
102
+ },
103
+ ],
104
+ },
105
+ },
106
+ {
107
+ headers: {
108
+ Authorization: `Bearer ${user.access_token}`,
109
+ },
110
+ }
111
+ );
112
+ formId = form.id;
113
+ }
114
+
115
+ const { data } = await axios.get<{
116
+ id: string;
117
+ data: <%= className %>;
118
+ }>(`${FORM_SERVICE_URL}/form/v1/forms/${formId}/data`, {
119
+ headers: {
120
+ Authorization: `Bearer ${user.access_token}`,
121
+ },
122
+ });
123
+
124
+ return data;
125
+ }
126
+ );
127
+
128
+ const formSaved = createAction(
129
+ '<%= propertyName %>/saved',
130
+ (values: <%= className %>) => ({ payload: values })
131
+ );
132
+
133
+ const queueSaveForm = createAsyncThunk(
134
+ '<%= propertyName %>/queue-save',
135
+ debounce(
136
+ async (values: <%= className %>, { dispatch, getState }) => {
137
+ const state = getState();
138
+ const { user }: UserState = state['user'];
139
+ const { formId }: <%= className %>State =
140
+ state[<%= constantName %>_FEATURE_KEY];
141
+
142
+ await axios.put(
143
+ `${FORM_SERVICE_URL}/form/v1/forms/${formId}/data`,
144
+ { data: values },
145
+ { headers: { Authorization: `Bearer ${user.access_token}` } }
146
+ );
147
+
148
+ dispatch(formSaved(values));
149
+ },
150
+ 2000,
151
+ { trailing: true, leading: false }
152
+ )
153
+ );
154
+
155
+ export const updateForm = createAsyncThunk(
156
+ '<%= propertyName %>/update',
157
+ async (values: <%= className %>, { dispatch }) => {
158
+ const errors: Record<string, Record<string, boolean>> = {};
159
+ let hasError = false;
160
+ Object.entries(rules).forEach(([section, sectionRules]) => {
161
+ const sectionValue = values[section] || {};
162
+ const sectionErrors = Object.entries(sectionRules).reduce(
163
+ (results, [key, valueRule]) => {
164
+ const value = sectionValue[key] || '';
165
+ const isError = !!value && !valueRule.test(value);
166
+ hasError = hasError || isError;
167
+ return { ...results, [key]: isError };
168
+ },
169
+ {}
170
+ );
171
+
172
+ errors[section] = sectionErrors;
173
+ });
174
+
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
+ });
189
+
190
+ if (!hasError) {
191
+ dispatch(queueSaveForm(values));
192
+ }
193
+
194
+ return { values, complete, errors };
195
+ }
196
+ );
197
+
198
+ export const submitForm = createAsyncThunk(
199
+ '<%= propertyName %>/submit',
200
+ async (formId: string) => {
201
+ const { data } = await axios.post(
202
+ `${FORM_SERVICE_URL}/form/v1/forms/${formId}`,
203
+ { operation: 'submit' },
204
+ { headers: {} }
205
+ );
206
+ return data;
207
+ }
208
+ );
209
+
210
+ export const initial<%= className %>State: <%= className %>State = {
211
+ formId: null,
212
+ values: {} as <%= className %>,
213
+ errors: {},
214
+ complete: {},
215
+ review: false,
216
+ busy: {
217
+ loading: false,
218
+ saving: false,
219
+ submitting: false,
220
+ },
221
+ };
222
+
223
+ export const <%= propertyName %>Slice = createSlice({
224
+ name: <%= constantName %>_FEATURE_KEY,
225
+ initialState: initial<%= className %>State,
226
+ reducers: {},
227
+ extraReducers: (builder) => {
228
+ builder
229
+ .addCase(initializeForm.pending, (state) => {
230
+ state.busy.loading = true;
231
+ })
232
+ .addCase(initializeForm.fulfilled, (state, action) => {
233
+ state.formId = action.payload.id;
234
+ state.values = action.payload.data;
235
+ state.busy.loading = false;
236
+ })
237
+ .addCase(initializeForm.rejected, (state) => {
238
+ state.busy.loading = false;
239
+ })
240
+ .addCase(updateForm.fulfilled, (state, action) => {
241
+ state.values = action.payload.values;
242
+ state.complete = action.payload.complete;
243
+ state.errors = action.payload.errors;
244
+ })
245
+ .addCase(queueSaveForm.pending, (state) => {
246
+ state.busy.saving = true;
247
+ })
248
+ .addCase(formSaved, (state) => {
249
+ state.busy.saving = false;
250
+ })
251
+ .addCase(queueSaveForm.rejected, (state) => {
252
+ state.busy.saving = false;
253
+ });
254
+ },
255
+ });
256
+
257
+ /*
258
+ * Export reducer for store configuration.
259
+ */
260
+ export const <%= propertyName %>Reducer = <%= propertyName %>Slice.reducer;
261
+
262
+ /*
263
+ * Export action creators to be dispatched. For use with the `useDispatch` hook.
264
+ *
265
+ * e.g.
266
+ * ```
267
+ * import React, { useEffect } from 'react';
268
+ * import { useDispatch } from 'react-redux';
269
+ *
270
+ * // ...
271
+ *
272
+ * const dispatch = useDispatch();
273
+ * useEffect(() => {
274
+ * dispatch(intakeActions.add({ id: 1 }))
275
+ * }, [dispatch]);
276
+ * ```
277
+ *
278
+ * See: https://react-redux.js.org/next/api/hooks#usedispatch
279
+ */
280
+ export const <%= propertyName %>Actions = <%= propertyName %>Slice.actions;
281
+
282
+ /*
283
+ * Export selectors to query state. For use with the `useSelector` hook.
284
+ *
285
+ * e.g.
286
+ * ```
287
+ * import { useSelector } from 'react-redux';
288
+ *
289
+ * // ...
290
+ *
291
+ * const entities = useSelector(selectAllIntake);
292
+ * ```
293
+ *
294
+ * See: https://react-redux.js.org/next/api/hooks#useselector
295
+ */
296
+ // const { selectAll, selectEntities } = intakeAdapter.getSelectors();
297
+
298
+ export const get<%= className %>State = (
299
+ rootState: unknown
300
+ ): <%= className %>State => rootState[<%= constantName %>_FEATURE_KEY];
301
+
302
+ export const getFormValues = createSelector(
303
+ get<%= className %>State,
304
+ (state) => state.values
305
+ );
306
+
307
+ export const getFormErrors = createSelector(
308
+ get<%= className %>State,
309
+ (state) => state.errors
310
+ );
311
+
312
+ export const getFormBusy = createSelector(
313
+ get<%= className %>State,
314
+ (state) => state.busy
315
+ );
316
+
317
+ export const getFormComplete = createSelector(
318
+ get<%= className %>State,
319
+ (state) => state.complete
320
+ );
@@ -0,0 +1,121 @@
1
+ import {
2
+ GoAButton,
3
+ GoACheckbox,
4
+ GoAFormItem,
5
+ GoAFormStep,
6
+ GoAFormStepper,
7
+ GoAInput,
8
+ GoASpinner,
9
+ } from '@abgov/react-components';
10
+ import { FunctionComponent, useEffect } from 'react';
11
+ import { useDispatch, useSelector } from 'react-redux';
12
+ import { AppDispatch } from '../../store';
13
+ import {
14
+ getFormBusy,
15
+ getFormComplete,
16
+ getFormErrors,
17
+ getFormValues,
18
+ initializeForm,
19
+ updateForm,
20
+ } from './<%= fileName %>.slice';
21
+ import styles from './<%= fileName %>.module.css';
22
+
23
+ interface FieldSetProps {
24
+ value: Record<string, unknown>;
25
+ errors: Record<string, boolean>;
26
+ onChange: (value: Record<string, unknown>) => void;
27
+ }
28
+
29
+ <% Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { %>
30
+ const <%= sectionKey %>FieldSet: FunctionComponent<FieldSetProps> = ({
31
+ value,
32
+ errors,
33
+ onChange,
34
+ }) => {
35
+ return (
36
+ <fieldset>
37
+ <legend><%= section.title || sectionKey %></legend>
38
+ <%_ 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>
70
+ <%_ }); _%>
71
+ </fieldset>
72
+ );
73
+ };
74
+ <% }); %>
75
+
76
+ export const <%= className %>Form: FunctionComponent = () => {
77
+ const dispatch = useDispatch<AppDispatch>();
78
+ const user = useSelector((state) => state['user'].user);
79
+ useEffect(() => {
80
+ dispatch(initializeForm());
81
+ }, [dispatch, user]);
82
+
83
+ const formData = useSelector(getFormValues);
84
+ const formErrors = useSelector(getFormErrors);
85
+ const formBusy = useSelector(getFormBusy);
86
+ const formComplete = useSelector(getFormComplete);
87
+
88
+ return (
89
+ <form>
90
+ <GoAFormStepper testId="<%= fileName %>>">
91
+ <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
92
+ <GoAFormStep
93
+ text="<%= section.title || sectionKey %>"
94
+ status={formComplete['<%= sectionKey %>'] ? 'complete' : null} />
95
+ <%_ }); _%>
96
+ <GoAFormStep text="Review" />
97
+ </GoAFormStepper>
98
+ {formBusy.loading ? (
99
+ <div className={styles.load}>
100
+ <GoASpinner type="infinite" size="large" />
101
+ </div>
102
+ ) : (
103
+ <>
104
+ <%_ Object.entries(dataSchema.properties).forEach(function([sectionKey, section]) { _%>
105
+ <<%= section.className %>FieldSet
106
+ value={formData.<%= sectionKey %> as any || {}}
107
+ errors={formErrors['<%= sectionKey %>'] || {}}
108
+ onChange={(value) =>
109
+ dispatch(updateForm({ ...formData, '<%= sectionKey %>': value as any }))
110
+ }
111
+ />
112
+ <%_ }); _%>
113
+ </>
114
+ )}
115
+ <div className={styles.formActions}>
116
+ <GoAButton type="secondary">Back</GoAButton>
117
+ <GoAButton type="primary">Next</GoAButton>
118
+ </div>
119
+ </form>
120
+ );
121
+ };
@@ -0,0 +1,3 @@
1
+ import { Tree } from '@nrwl/devkit';
2
+ import { Schema } from './schema';
3
+ export default function (host: Tree, options: Schema): Promise<() => void>;
@@ -0,0 +1,89 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const tslib_1 = require("tslib");
4
+ const nx_oc_1 = require("@abgov/nx-oc");
5
+ const devkit_1 = require("@nrwl/devkit");
6
+ const axios_1 = require("axios");
7
+ const enquirer_1 = require("enquirer");
8
+ const path = require("path");
9
+ const form_1 = require("../../utils/form");
10
+ function getFormDefinition(formServiceUrl, token) {
11
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
12
+ const { data: definitions } = yield axios_1.default.get(new URL('form/v1/definitions', formServiceUrl).href, {
13
+ headers: { Authorization: `Bearer ${token}` },
14
+ });
15
+ const choices = definitions
16
+ .filter((r) => !!r.dataSchema)
17
+ .map((r) => r.name)
18
+ .sort((a, b) => a.localeCompare(b));
19
+ if (choices.length < 1) {
20
+ throw new Error('No form definitions with data schema found.');
21
+ }
22
+ const result = yield (0, enquirer_1.prompt)({
23
+ type: 'autocomplete',
24
+ name: 'definition',
25
+ message: 'Which form definition do you want to generate a component for?',
26
+ choices,
27
+ });
28
+ const formDefinition = definitions.find((r) => r.name === result.definition);
29
+ const general = {
30
+ type: 'object',
31
+ className: 'General',
32
+ properties: {},
33
+ };
34
+ let addGeneral = false;
35
+ Object.entries(formDefinition.dataSchema.properties || {}).forEach(([property, value]) => {
36
+ if (value.type !== 'object') {
37
+ general.properties[property] = value;
38
+ delete formDefinition.dataSchema.properties[property];
39
+ addGeneral = true;
40
+ }
41
+ else {
42
+ value.className = (0, devkit_1.names)(property).className;
43
+ }
44
+ });
45
+ if (addGeneral) {
46
+ formDefinition.dataSchema.properties = Object.assign({ general }, formDefinition.dataSchema.properties);
47
+ }
48
+ return formDefinition;
49
+ });
50
+ }
51
+ function normalizeOptions(host, options) {
52
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
53
+ const { env, accessToken } = options;
54
+ const projectName = (0, devkit_1.names)(options.project).fileName;
55
+ const projectRoot = `${(0, devkit_1.getWorkspaceLayout)(host).appsDir}/${projectName}`;
56
+ const environment = nx_oc_1.environments[env || 'prod'];
57
+ const urls = yield (0, nx_oc_1.getServiceUrls)(environment.directoryServiceUrl);
58
+ let tenantToken = accessToken;
59
+ if (!accessToken) {
60
+ const token = yield (0, nx_oc_1.realmLogin)(environment.accessServiceUrl, 'core');
61
+ const tenant = yield (0, nx_oc_1.selectTenant)(urls['urn:ads:platform:tenant-service'], token);
62
+ tenantToken = yield (0, nx_oc_1.realmLogin)(environment.accessServiceUrl, tenant.realm);
63
+ }
64
+ const formServiceUrl = urls['urn:ads:platform:form-service'];
65
+ const formDefinition = yield getFormDefinition(formServiceUrl, tenantToken);
66
+ return Object.assign(Object.assign({}, options), { projectRoot,
67
+ formServiceUrl,
68
+ formDefinition });
69
+ });
70
+ }
71
+ function addFiles(host, options) {
72
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
73
+ const formNames = (0, devkit_1.names)(options.formDefinition.name);
74
+ const interfaceDefinition = yield (0, form_1.generateFormInterface)(options.formDefinition);
75
+ const templateOptions = Object.assign(Object.assign(Object.assign(Object.assign({}, options), options.formDefinition), formNames), { interfaceDefinition, tmpl: '' });
76
+ (0, devkit_1.generateFiles)(host, path.join(__dirname, 'files'), options.projectRoot, templateOptions);
77
+ });
78
+ }
79
+ function default_1(host, options) {
80
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
81
+ const normalizedOptions = yield normalizeOptions(host, options);
82
+ yield addFiles(host, normalizedOptions);
83
+ return () => {
84
+ (0, devkit_1.formatFiles)(host);
85
+ };
86
+ });
87
+ }
88
+ exports.default = default_1;
89
+ //# sourceMappingURL=react-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"react-form.js","sourceRoot":"","sources":["../../../../../../packages/nx-adsp/src/generators/react-form/react-form.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,94 @@
1
+ import * as utils from '@abgov/nx-oc';
2
+ import { addProjectConfiguration } from '@nrwl/devkit';
3
+ import { createTreeWithEmptyV1Workspace } from '@nrwl/devkit/testing';
4
+ import axios from 'axios';
5
+ import { prompt } from 'enquirer';
6
+
7
+ import { FormDefinition } from '../../utils/form';
8
+ import { Schema } from './schema';
9
+ import generator from './react-form';
10
+
11
+ const formDefinition: FormDefinition = {
12
+ name: 'Some Intake',
13
+ dataSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ notInSection: {
17
+ type: 'string',
18
+ },
19
+ personalInformation: {
20
+ title: 'Personal information',
21
+ description: '',
22
+ type: 'object',
23
+ properties: {
24
+ firstName: {
25
+ type: 'string',
26
+ pattern: '^.{1,20}$',
27
+ },
28
+ lastName: {
29
+ type: 'string',
30
+ },
31
+ },
32
+ required: ['firstName', 'lastName'],
33
+ },
34
+ additional: {
35
+ description: 'Provide additional information.',
36
+ type: 'object',
37
+ properties: {
38
+ income: {
39
+ type: 'number',
40
+ title: 'Annual income',
41
+ description: 'Annual income from line 4 of Notice of Assessment.',
42
+ },
43
+ consent: {
44
+ type: 'boolean',
45
+ },
46
+ },
47
+ },
48
+ },
49
+ },
50
+ };
51
+
52
+ jest.mock('@abgov/nx-oc');
53
+ const utilsMock = utils as jest.Mocked<typeof utils>;
54
+ utilsMock.getServiceUrls.mockResolvedValue({
55
+ 'urn:ads:platform:tenant-service': 'https://tenant-service',
56
+ 'urn:ads:platform:form-service': 'https://form-service',
57
+ });
58
+
59
+ utilsMock.realmLogin.mockResolvedValue('token');
60
+ utilsMock.selectTenant.mockResolvedValue({ name: 'demo', realm: 'demo' });
61
+
62
+ jest.mock('axios');
63
+ const axiosMock = axios as jest.Mocked<typeof axios>;
64
+ axiosMock.get.mockResolvedValueOnce({ data: [formDefinition] });
65
+
66
+ jest.mock('enquirer', () => ({ prompt: jest.fn() }));
67
+ const promptMock = prompt as jest.Mock;
68
+ promptMock.mockResolvedValue({ definition: 'Some Intake' });
69
+
70
+ describe('React Form Generator', () => {
71
+ const options: Schema = {
72
+ project: 'test',
73
+ env: 'dev',
74
+ };
75
+
76
+ it('can run', async () => {
77
+ const host = createTreeWithEmptyV1Workspace();
78
+
79
+ addProjectConfiguration(host, 'test', {
80
+ root: 'apps/test',
81
+ projectType: 'application',
82
+ targets: {
83
+ build: {
84
+ executor: '@nrwl/web:webpack',
85
+ },
86
+ },
87
+ });
88
+
89
+ await generator(host, options);
90
+ expect(host.exists('apps/test/some-intake/some-intake.tsx')).toBeTruthy();
91
+ expect(host.exists('apps/test/some-intake/some-intake.slice.ts')).toBeTruthy();
92
+ expect(host.exists('apps/test/some-intake/some-intake.module.css')).toBeTruthy();
93
+ }, 30000);
94
+ });
@@ -0,0 +1,13 @@
1
+ import { EnvironmentName } from '@abgov/nx-oc';
2
+
3
+ export interface Schema {
4
+ project: string;
5
+ env: EnvironmentName;
6
+ accessToken?: string;
7
+ }
8
+
9
+ export interface NormalizedSchema extends Schema {
10
+ projectRoot: string;
11
+ formDefinition: FormDefinition;
12
+ formServiceUrl: string;
13
+ }
@@ -0,0 +1,45 @@
1
+ {
2
+ "$schema": "http://json-schema.org/schema",
3
+ "cli": "nx",
4
+ "id": "NxAdspReactForm",
5
+ "title": "",
6
+ "type": "object",
7
+ "properties": {
8
+ "project": {
9
+ "type": "string",
10
+ "description": "Project to add form component in.",
11
+ "$default": {
12
+ "$source": "argv",
13
+ "index": 0
14
+ },
15
+ "x-prompt": "Which project to add form component for?"
16
+ },
17
+ "env": {
18
+ "type": "string",
19
+ "description": "Environment to target.",
20
+ "$default": {
21
+ "$source": "argv",
22
+ "index": 1
23
+ },
24
+ "alias": "e",
25
+ "x-prompt": {
26
+ "message": "Which ADSP environment do you want to target?",
27
+ "type": "list",
28
+ "items": [
29
+ "dev",
30
+ "test",
31
+ "prod"
32
+ ]
33
+ }
34
+ },
35
+ "accessToken": {
36
+ "type": "string",
37
+ "description": "Access token for retrieving configuration from ADSP APIs.",
38
+ "alias": "at"
39
+ }
40
+ },
41
+ "required": [
42
+ "project",
43
+ "env"
44
+ ]
45
+ }
@@ -0,0 +1,5 @@
1
+ export interface FormDefinition {
2
+ name: string;
3
+ dataSchema: Record<string, unknown>;
4
+ }
5
+ export declare function generateFormInterface({ name, dataSchema, }: FormDefinition): Promise<string>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateFormInterface = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const json_schema_to_typescript_1 = require("json-schema-to-typescript");
6
+ function generateFormInterface({ name, dataSchema, }) {
7
+ return tslib_1.__awaiter(this, void 0, void 0, function* () {
8
+ const types = yield (0, json_schema_to_typescript_1.compile)(dataSchema, name, {
9
+ additionalProperties: false,
10
+ });
11
+ return types;
12
+ });
13
+ }
14
+ exports.generateFormInterface = generateFormInterface;
15
+ //# sourceMappingURL=form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"form.js","sourceRoot":"","sources":["../../../../../packages/nx-adsp/src/utils/form.ts"],"names":[],"mappings":";;;;AAAA,yEAAoD;AAOpD,SAAsB,qBAAqB,CAAC,EAC1C,IAAI,EACJ,UAAU,GACK;;QACf,MAAM,KAAK,GAAG,MAAM,IAAA,mCAAO,EAAC,UAAU,EAAE,IAAI,EAAE;YAC5C,oBAAoB,EAAE,KAAK;SAC5B,CAAC,CAAC;QAEH,OAAO,KAAK,CAAC;IACf,CAAC;CAAA;AATD,sDASC"}
@@ -0,0 +1,47 @@
1
+ import { FormDefinition, generateFormInterface } from './form';
2
+
3
+ describe('generateFormInterface', () => {
4
+ const dataSchema = {
5
+ type: 'object',
6
+ properties: {
7
+ firstName: {
8
+ type: 'string',
9
+ },
10
+ lastName: {
11
+ type: 'string',
12
+ },
13
+ age: {
14
+ description: 'Age in years',
15
+ type: 'integer',
16
+ minimum: 0,
17
+ },
18
+ hairColor: {
19
+ enum: ['black', 'brown', 'blue'],
20
+ type: 'string',
21
+ },
22
+ personal: {
23
+ type: 'object',
24
+ properties: {
25
+ firstName: {
26
+ type: 'string',
27
+ },
28
+ lastName: {
29
+ type: 'string',
30
+ },
31
+ },
32
+ },
33
+ },
34
+ additionalProperties: false,
35
+ required: ['firstName', 'lastName'],
36
+ };
37
+
38
+ it('can generate form interface', async () => {
39
+ const result = await generateFormInterface({
40
+ name: 'Test questionnaire',
41
+ dataSchema: dataSchema as unknown,
42
+ } as FormDefinition);
43
+
44
+ expect(result).toBeTruthy();
45
+ // expect(result).toMatchSnapshot();
46
+ });
47
+ });