@aws-amplify/amplify-util-uibuilder 1.6.5 → 1.6.6-rc.4b3db16df.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/CHANGELOG.md +16 -0
- package/amplify-plugin.json +3 -1
- package/lib/clients/amplify-studio-client.d.ts +1 -0
- package/lib/clients/amplify-studio-client.d.ts.map +1 -1
- package/lib/clients/amplify-studio-client.js +13 -0
- package/lib/clients/amplify-studio-client.js.map +1 -1
- package/lib/commands/cloneComponentsFromEnv.js +2 -2
- package/lib/commands/cloneComponentsFromEnv.js.map +1 -1
- package/lib/commands/generateComponents.d.ts +1 -1
- package/lib/commands/generateComponents.d.ts.map +1 -1
- package/lib/commands/generateComponents.js +15 -3
- package/lib/commands/generateComponents.js.map +1 -1
- package/lib/commands/utils/detachedFormCleanup.d.ts +11 -0
- package/lib/commands/utils/detachedFormCleanup.d.ts.map +1 -0
- package/lib/commands/utils/detachedFormCleanup.js +44 -0
- package/lib/commands/utils/detachedFormCleanup.js.map +1 -0
- package/lib/commands/utils/index.d.ts +1 -0
- package/lib/commands/utils/index.d.ts.map +1 -1
- package/lib/commands/utils/index.js +1 -0
- package/lib/commands/utils/index.js.map +1 -1
- package/lib/commands/utils/shouldRenderComponents.d.ts.map +1 -1
- package/lib/commands/utils/shouldRenderComponents.js +7 -5
- package/lib/commands/utils/shouldRenderComponents.js.map +1 -1
- package/lib/commands/utils/syncAmplifyUiBuilderComponents.d.ts.map +1 -1
- package/lib/commands/utils/syncAmplifyUiBuilderComponents.js +3 -1
- package/lib/commands/utils/syncAmplifyUiBuilderComponents.js.map +1 -1
- package/lib/event-handlers/handle-PostPull.js +1 -1
- package/lib/event-handlers/handle-PostPull.js.map +1 -1
- package/lib/event-handlers/handle-PostPush.js +1 -1
- package/lib/event-handlers/handle-PostPush.js.map +1 -1
- package/lib/event-handlers/handle-PrePush.d.ts +3 -0
- package/lib/event-handlers/handle-PrePush.d.ts.map +1 -0
- package/lib/event-handlers/handle-PrePush.js +15 -0
- package/lib/event-handlers/handle-PrePush.js.map +1 -0
- package/lib/utils/prePushHandler.d.ts +3 -0
- package/lib/utils/prePushHandler.d.ts.map +1 -0
- package/lib/utils/prePushHandler.js +35 -0
- package/lib/utils/prePushHandler.js.map +1 -0
- package/package.json +8 -6
- package/src/__tests__/detachedFormCleanup.test.ts +78 -0
- package/src/__tests__/generateComponents.test.ts +13 -11
- package/src/__tests__/handleEvents.test.ts +27 -0
- package/src/__tests__/prePushHandler.test.ts +105 -0
- package/src/__tests__/shouldRenderComponents.test.ts +48 -35
- package/src/__tests__/syncAmplifyUiComponents.test.ts +3 -3
- package/src/clients/amplify-studio-client.ts +13 -0
- package/src/commands/cloneComponentsFromEnv.ts +1 -1
- package/src/commands/generateComponents.ts +30 -4
- package/src/commands/utils/detachedFormCleanup.ts +55 -0
- package/src/commands/utils/index.ts +1 -0
- package/src/commands/utils/shouldRenderComponents.ts +7 -5
- package/src/commands/utils/syncAmplifyUiBuilderComponents.ts +3 -1
- package/src/event-handlers/handle-PostPull.ts +1 -1
- package/src/event-handlers/handle-PostPush.ts +1 -1
- package/src/event-handlers/handle-PrePush.ts +15 -0
- package/src/utils/prePushHandler.ts +51 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// @aws-amplify/codegen-ui is in project dependencies
|
|
2
|
+
import { StudioSchema } from '@aws-amplify/codegen-ui'; // eslint-disable-line import/no-extraneous-dependencies
|
|
3
|
+
import {AmplifyStudioClient} from '../clients'; // eslint-disable-line import/no-extraneous-dependencies
|
|
4
|
+
import {
|
|
5
|
+
isFormDetachedFromModel, isFormSchemaCustomized, isStudioForm, deleteDetachedForms
|
|
6
|
+
} from '../commands/utils';
|
|
7
|
+
|
|
8
|
+
const amplifyStudioClientMock = AmplifyStudioClient as any;
|
|
9
|
+
|
|
10
|
+
const formWithNoCustomization: StudioSchema = {
|
|
11
|
+
id: 'f-123456',
|
|
12
|
+
name: 'BlogCreateForm',
|
|
13
|
+
formActionType: 'create',
|
|
14
|
+
dataType: {
|
|
15
|
+
dataSourceType: 'DataStore',
|
|
16
|
+
dataTypeName: 'Blog',
|
|
17
|
+
},
|
|
18
|
+
fields: {},
|
|
19
|
+
sectionalElements: {},
|
|
20
|
+
style: {},
|
|
21
|
+
cta: {},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const formWithStyle: StudioSchema = {
|
|
25
|
+
...formWithNoCustomization,
|
|
26
|
+
dataType: { dataSourceType: 'Custom', dataTypeName: 'Blog' },
|
|
27
|
+
style: {
|
|
28
|
+
horizontalGap: { value: '20px' },
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const formWithFields: StudioSchema = {
|
|
33
|
+
...formWithNoCustomization,
|
|
34
|
+
dataType: { dataSourceType: 'Custom', dataTypeName: 'Blog' },
|
|
35
|
+
fields: {
|
|
36
|
+
name: { inputType: { type: 'TextField' } },
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
describe('detachedFormCleanup', () => {
|
|
41
|
+
|
|
42
|
+
it('isStudioForm should return true', () => {
|
|
43
|
+
expect(isStudioForm(formWithNoCustomization)).toBe(true);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('isStudioForm should return false', () => {
|
|
47
|
+
expect(isStudioForm(undefined)).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('isFormSchemaCustomized should return false', () => {
|
|
51
|
+
expect(isFormSchemaCustomized(formWithNoCustomization)).toBe(false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('isFormSchemaCustomized should return true', () => {
|
|
55
|
+
expect(isFormSchemaCustomized(formWithStyle)).toBe(true);
|
|
56
|
+
expect(isFormSchemaCustomized(formWithFields)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('isFormDetachedFromModel should return true for a DataStore form with detached model', () => {
|
|
60
|
+
expect(isFormDetachedFromModel(formWithNoCustomization, new Set<string>())).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('isFormDetachedFromModel should return false for a Custom form', () => {
|
|
64
|
+
expect(isFormDetachedFromModel(formWithStyle, new Set<string>())).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('isFormDetachedFromModel should return false for a DataStore form with an attached model', () => {
|
|
68
|
+
expect(isFormDetachedFromModel(formWithNoCustomization, new Set<string>().add('Blog'))).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should delete detached forms', async () => {
|
|
72
|
+
const mockedDeleteForm = jest.fn().mockReturnValue(undefined);
|
|
73
|
+
amplifyStudioClientMock.deleteForm = mockedDeleteForm;
|
|
74
|
+
await deleteDetachedForms([{id: '12345', name: 'BlogCreateForm'}], amplifyStudioClientMock);
|
|
75
|
+
expect(mockedDeleteForm).toBeCalledTimes(1);
|
|
76
|
+
expect(mockedDeleteForm).toHaveBeenCalledWith('12345');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
@@ -9,8 +9,10 @@ const utilsMock = utils as any;
|
|
|
9
9
|
|
|
10
10
|
utilsMock.shouldRenderComponents = jest.fn().mockReturnValue(true);
|
|
11
11
|
utilsMock.notifyMissingPackages = jest.fn().mockReturnValue(true);
|
|
12
|
+
utilsMock.getAmplifyDataSchema = jest.fn().mockReturnValue(({}));
|
|
13
|
+
|
|
12
14
|
jest.mock('../commands/utils/featureFlags', () => ({
|
|
13
|
-
getTransformerVersion: jest.fn().
|
|
15
|
+
getTransformerVersion: jest.fn().mockReturnValue(2),
|
|
14
16
|
}));
|
|
15
17
|
|
|
16
18
|
describe('can generate components', () => {
|
|
@@ -37,6 +39,13 @@ describe('can generate components', () => {
|
|
|
37
39
|
name: 'testSchema',
|
|
38
40
|
schemaVersion: '1.0',
|
|
39
41
|
},
|
|
42
|
+
{
|
|
43
|
+
resultType: 'FAILURE',
|
|
44
|
+
schemaName: 'testSchema',
|
|
45
|
+
name: 'testSchema',
|
|
46
|
+
schemaVersion: '1.0',
|
|
47
|
+
schema: { id: 'f-123456', name: 'testSchema' },
|
|
48
|
+
},
|
|
40
49
|
],
|
|
41
50
|
};
|
|
42
51
|
mockedExport = jest.fn().mockReturnValue({
|
|
@@ -70,22 +79,15 @@ describe('can generate components', () => {
|
|
|
70
79
|
utilsMock.getAmplifyDataSchema = jest.fn().mockReturnValue(undefined);
|
|
71
80
|
utilsMock.generateAmplifyUiBuilderIndexFile = jest.fn().mockReturnValue(true);
|
|
72
81
|
utilsMock.generateAmplifyUiBuilderUtilFile = jest.fn().mockReturnValue(true);
|
|
82
|
+
utilsMock.deleteDetachedForms = jest.fn();
|
|
73
83
|
});
|
|
74
84
|
|
|
75
85
|
it('runs generateComponents', async () => {
|
|
76
|
-
await run(context);
|
|
86
|
+
await run(context, 'PostPull');
|
|
77
87
|
expect(mockedExport).toBeCalledTimes(3);
|
|
78
88
|
expect(utilsMock.generateUiBuilderComponents).toBeCalledTimes(1);
|
|
79
89
|
expect(utilsMock.generateUiBuilderThemes).toBeCalledTimes(1);
|
|
80
90
|
expect(utilsMock.generateUiBuilderForms).toBeCalledTimes(1);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
it('does not run generateComponents if not Amplify Admin app', async () => {
|
|
84
|
-
utilsMock.shouldRenderComponents = jest.fn().mockReturnValue(false);
|
|
85
|
-
await run(context);
|
|
86
|
-
expect(mockedExport).toBeCalledTimes(0);
|
|
87
|
-
expect(utilsMock.generateUiBuilderComponents).toBeCalledTimes(0);
|
|
88
|
-
expect(utilsMock.generateUiBuilderThemes).toBeCalledTimes(0);
|
|
89
|
-
expect(utilsMock.generateUiBuilderForms).toBeCalledTimes(0);
|
|
91
|
+
expect(utilsMock.deleteDetachedForms).toBeCalledTimes(1);
|
|
90
92
|
});
|
|
91
93
|
});
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { run as runPostPull } from '../event-handlers/handle-PostPull';
|
|
2
|
+
import { run as runPostPush } from '../event-handlers/handle-PostPush';
|
|
3
|
+
import { run as generateComponents } from '../commands/generateComponents';
|
|
4
|
+
|
|
5
|
+
jest.mock('../commands/generateComponents');
|
|
6
|
+
|
|
7
|
+
describe('can handle events', () => {
|
|
8
|
+
let context: any;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
context = {
|
|
12
|
+
amplify: {
|
|
13
|
+
invokePluginMethod: jest.fn(),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('handles postPull', async () => {
|
|
19
|
+
await runPostPull(context);
|
|
20
|
+
expect(generateComponents).toHaveBeenCalledWith(context, 'PostPull');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('handles postPush', async () => {
|
|
24
|
+
await runPostPush(context);
|
|
25
|
+
expect(generateComponents).toHaveBeenCalledWith(context, 'PostPush');
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// aws-sdk, amplify-cli-core, amplify-prompts are in package dependencies
|
|
2
|
+
import aws from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies
|
|
3
|
+
import { Form } from 'aws-sdk/clients/amplifyuibuilder'; // eslint-disable-line import/no-extraneous-dependencies
|
|
4
|
+
import { printer } from 'amplify-prompts'; // eslint-disable-line import/no-extraneous-dependencies
|
|
5
|
+
import * as utils from '../commands/utils';
|
|
6
|
+
import { prePushHandler } from '../utils/prePushHandler';
|
|
7
|
+
|
|
8
|
+
jest.mock('../commands/utils');
|
|
9
|
+
jest.mock('amplify-cli-core');
|
|
10
|
+
jest.mock('../commands/utils/featureFlags', () => ({
|
|
11
|
+
getTransformerVersion: jest.fn().mockImplementation(() => 2),
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
const awsMock = aws as any;
|
|
15
|
+
const utilsMock = utils as any;
|
|
16
|
+
|
|
17
|
+
utilsMock.shouldRenderComponents = jest.fn().mockImplementation(() => true);
|
|
18
|
+
|
|
19
|
+
describe('handlePrePush', () => {
|
|
20
|
+
let context: any;
|
|
21
|
+
let mockedExport: jest.Mock<any, any>;
|
|
22
|
+
let exportedForms: Form[];
|
|
23
|
+
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
context = {
|
|
26
|
+
amplify: {
|
|
27
|
+
invokePluginMethod: () => ({ models: { Comment: {} } }),
|
|
28
|
+
},
|
|
29
|
+
input: {
|
|
30
|
+
options: {
|
|
31
|
+
appId: 'testAppId',
|
|
32
|
+
envName: 'testEnvName',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
exportedForms = [
|
|
38
|
+
{
|
|
39
|
+
name: 'BlogCreateForm',
|
|
40
|
+
formActionType: 'create',
|
|
41
|
+
fields: {},
|
|
42
|
+
style: {},
|
|
43
|
+
schemaVersion: '1.0',
|
|
44
|
+
sectionalElements: {},
|
|
45
|
+
appId: '1234',
|
|
46
|
+
environmentName: 'staging',
|
|
47
|
+
dataType: {
|
|
48
|
+
dataSourceType: 'DataStore',
|
|
49
|
+
dataTypeName: 'Blog',
|
|
50
|
+
},
|
|
51
|
+
id: '12345',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: 'BlogUpdateForm',
|
|
55
|
+
formActionType: 'update',
|
|
56
|
+
fields: {},
|
|
57
|
+
style: {},
|
|
58
|
+
schemaVersion: '1.0',
|
|
59
|
+
sectionalElements: {},
|
|
60
|
+
appId: '1234',
|
|
61
|
+
environmentName: 'staging',
|
|
62
|
+
dataType: {
|
|
63
|
+
dataSourceType: 'DataStore',
|
|
64
|
+
dataTypeName: 'Blog',
|
|
65
|
+
},
|
|
66
|
+
id: '12345',
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
mockedExport = jest.fn(() => ({
|
|
71
|
+
entities: exportedForms,
|
|
72
|
+
}));
|
|
73
|
+
|
|
74
|
+
awsMock.AmplifyUIBuilder = jest.fn(() => ({
|
|
75
|
+
exportForms: jest.fn(() => ({
|
|
76
|
+
promise: () => mockedExport(),
|
|
77
|
+
})),
|
|
78
|
+
getMetadata: jest.fn(() => ({
|
|
79
|
+
promise: jest.fn(() => ({
|
|
80
|
+
features: {
|
|
81
|
+
autoGenerateForms: 'true',
|
|
82
|
+
autoGenerateViews: 'true',
|
|
83
|
+
},
|
|
84
|
+
})),
|
|
85
|
+
})),
|
|
86
|
+
}));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('runs handlePrePush', async () => {
|
|
90
|
+
utilsMock.isFormDetachedFromModel = jest.fn().mockImplementation(() => true);
|
|
91
|
+
utilsMock.isFormSchemaCustomized = jest.fn().mockImplementation(() => true);
|
|
92
|
+
const spy = jest.spyOn(printer, 'warn');
|
|
93
|
+
await prePushHandler(context);
|
|
94
|
+
expect(spy).toHaveBeenCalledWith('The following forms will no longer be available because the connected data model no longer exists: BlogCreateForm, BlogUpdateForm');
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('runs handlePrePush without schema', async () => {
|
|
98
|
+
context.amplify.invokePluginMethod = jest.fn()
|
|
99
|
+
.mockImplementation(() => ({ models: { Comment: {} } }))
|
|
100
|
+
.mockImplementation(() => (null));
|
|
101
|
+
const spy = jest.spyOn(printer, 'debug');
|
|
102
|
+
await prePushHandler(context);
|
|
103
|
+
expect(spy).toHaveBeenCalledWith('Local schema not found');
|
|
104
|
+
});
|
|
105
|
+
});
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
// eslint-disable-next-line import/no-extraneous-dependencies
|
|
2
|
-
import {
|
|
3
|
-
$TSContext, AmplifyCategories, AmplifySupportedService, CloudformationProviderFacade,
|
|
4
|
-
} from 'amplify-cli-core';
|
|
2
|
+
import {$TSContext} from 'amplify-cli-core';
|
|
5
3
|
import aws from 'aws-sdk'; // eslint-disable-line import/no-extraneous-dependencies
|
|
6
4
|
import { shouldRenderComponents } from '../commands/utils/shouldRenderComponents';
|
|
7
5
|
|
|
@@ -10,26 +8,21 @@ const awsMock = aws as any;
|
|
|
10
8
|
jest.mock('amplify-cli-core', () => ({
|
|
11
9
|
...jest.requireActual('amplify-cli-core'),
|
|
12
10
|
stateManager: {
|
|
13
|
-
|
|
14
|
-
[
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
getProjectConfig: jest.fn().mockReturnValue({
|
|
12
|
+
providers: ['awscloudformation'],
|
|
13
|
+
frontend: 'javascript',
|
|
14
|
+
javascript: {
|
|
15
|
+
framework: 'react',
|
|
18
16
|
},
|
|
19
|
-
})
|
|
20
|
-
// eslint-disable-next-line spellcheck/spell-checker
|
|
21
|
-
getCLIJSON: jest.fn().mockReturnValue({ features: { graphqltransformer: { transformerversion: 2 } } }),
|
|
22
|
-
setCLIJSON: jest.fn(),
|
|
23
|
-
},
|
|
24
|
-
FeatureFlags: {
|
|
25
|
-
getBoolean: () => true,
|
|
26
|
-
getNumber: jest.fn().mockReturnValue(2),
|
|
27
|
-
reloadValues: jest.fn(),
|
|
17
|
+
})
|
|
28
18
|
},
|
|
29
19
|
}));
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
20
|
+
|
|
21
|
+
jest.mock('../clients', () => ({
|
|
22
|
+
AmplifyStudioClient: {
|
|
23
|
+
isAmplifyApp: jest.fn().mockReturnValue(true)
|
|
24
|
+
}
|
|
25
|
+
}));
|
|
33
26
|
|
|
34
27
|
describe('should render components', () => {
|
|
35
28
|
let context: $TSContext | any;
|
|
@@ -47,9 +40,6 @@ describe('should render components', () => {
|
|
|
47
40
|
})),
|
|
48
41
|
}));
|
|
49
42
|
context = {
|
|
50
|
-
amplify: {
|
|
51
|
-
invokePluginMethod: () => ({}),
|
|
52
|
-
},
|
|
53
43
|
input: {
|
|
54
44
|
options: {
|
|
55
45
|
'no-codegen': false,
|
|
@@ -57,38 +47,61 @@ describe('should render components', () => {
|
|
|
57
47
|
envName: 'testEnvName',
|
|
58
48
|
},
|
|
59
49
|
},
|
|
60
|
-
exeInfo: {
|
|
61
|
-
projectConfig: {
|
|
62
|
-
providers: ['awscloudformation'],
|
|
63
|
-
frontend: 'javascript',
|
|
64
|
-
javascript: {
|
|
65
|
-
framework: 'react',
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
50
|
};
|
|
70
51
|
});
|
|
52
|
+
|
|
71
53
|
it('works with a valid config', async () => {
|
|
72
54
|
const shouldIt = await shouldRenderComponents(context);
|
|
73
55
|
expect(shouldIt).toBe(true);
|
|
74
56
|
});
|
|
57
|
+
|
|
75
58
|
it("doesn't work if --no-codegen flag is set", async () => {
|
|
76
59
|
context.input.options['no-codegen'] = true;
|
|
77
60
|
const shouldIt = await shouldRenderComponents(context);
|
|
78
61
|
expect(shouldIt).toBe(false);
|
|
79
62
|
});
|
|
63
|
+
|
|
80
64
|
it("doesn't work if provider is not awscloudformation", async () => {
|
|
81
|
-
|
|
65
|
+
jest.mock('amplify-cli-core', () => ({
|
|
66
|
+
stateManager: {
|
|
67
|
+
getProjectConfig: jest.fn().mockReturnValue({
|
|
68
|
+
providers: [],
|
|
69
|
+
frontend: 'javascript',
|
|
70
|
+
javascript: {
|
|
71
|
+
framework: 'react',
|
|
72
|
+
},
|
|
73
|
+
})
|
|
74
|
+
}
|
|
75
|
+
}));
|
|
82
76
|
const shouldIt = await shouldRenderComponents(context);
|
|
83
77
|
expect(shouldIt).toBe(false);
|
|
84
78
|
});
|
|
79
|
+
|
|
85
80
|
it('should return false if frontend is ios', async () => {
|
|
86
|
-
|
|
81
|
+
jest.mock('amplify-cli-core', () => ({
|
|
82
|
+
stateManager: {
|
|
83
|
+
getProjectConfig: jest.fn().mockReturnValue({
|
|
84
|
+
providers: [],
|
|
85
|
+
frontend: 'ios',
|
|
86
|
+
})
|
|
87
|
+
},
|
|
88
|
+
}));
|
|
87
89
|
const shouldIt = await shouldRenderComponents(context);
|
|
88
90
|
expect(shouldIt).toBe(false);
|
|
89
91
|
});
|
|
92
|
+
|
|
90
93
|
it('should return false if frontend is vue', async () => {
|
|
91
|
-
|
|
94
|
+
jest.mock('amplify-cli-core', () => ({
|
|
95
|
+
stateManager: {
|
|
96
|
+
getProjectConfig: jest.fn().mockReturnValue({
|
|
97
|
+
providers: [],
|
|
98
|
+
frontend: 'javascript',
|
|
99
|
+
javascript: {
|
|
100
|
+
framework: 'vue',
|
|
101
|
+
},
|
|
102
|
+
})
|
|
103
|
+
},
|
|
104
|
+
}));
|
|
92
105
|
const shouldIt = await shouldRenderComponents(context);
|
|
93
106
|
expect(shouldIt).toBe(false);
|
|
94
107
|
});
|
|
@@ -262,7 +262,7 @@ describe('should sync amplify ui builder components', () => {
|
|
|
262
262
|
const forms = generateUiBuilderForms(context, [{}, {}]);
|
|
263
263
|
expect(forms.every(form => form.resultType === 'FAILURE')).toBeTruthy();
|
|
264
264
|
});
|
|
265
|
-
it('can generate uibuilder forms from data schema if
|
|
265
|
+
it('can generate uibuilder forms from data schema if autogenerate true', async () => {
|
|
266
266
|
createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(() => ({}));
|
|
267
267
|
const forms = generateUiBuilderForms(context, [], exampleSchema, true);
|
|
268
268
|
expect(forms.every(form => form.resultType === 'SUCCESS')).toBeTruthy();
|
|
@@ -270,7 +270,7 @@ describe('should sync amplify ui builder components', () => {
|
|
|
270
270
|
expect(forms.length).toEqual(2);
|
|
271
271
|
});
|
|
272
272
|
|
|
273
|
-
it('should not
|
|
273
|
+
it('should not autogenerate forms for join tables or unsupported models', async () => {
|
|
274
274
|
expect(Object.keys(exampleSchema.models)).toStrictEqual(['Author', 'JoinTable', 'EmptyModel']);
|
|
275
275
|
createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(
|
|
276
276
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
@@ -282,7 +282,7 @@ describe('should sync amplify ui builder components', () => {
|
|
|
282
282
|
expect(forms.map(f => (f.schema as any).name)).toStrictEqual(['Author', 'Author']);
|
|
283
283
|
});
|
|
284
284
|
|
|
285
|
-
it('does not generate uibuilder forms from data schema if
|
|
285
|
+
it('does not generate uibuilder forms from data schema if autogenerate false', async () => {
|
|
286
286
|
createUiBuilderComponentDependencyMock.createUiBuilderForm = jest.fn().mockImplementation(() => ({}));
|
|
287
287
|
const forms = generateUiBuilderForms(context, [], exampleSchema, false);
|
|
288
288
|
expect(forms.length).toEqual(0);
|
|
@@ -230,6 +230,19 @@ export default class AmplifyStudioClient {
|
|
|
230
230
|
}
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
+
deleteForm = async (formId: string, envName?: string, appId?: string): Promise<void> => {
|
|
234
|
+
const environmentName = envName || this.#envName;
|
|
235
|
+
const resolvedAppId = appId || this.#appId;
|
|
236
|
+
try {
|
|
237
|
+
await this.#amplifyUiBuilder
|
|
238
|
+
.deleteForm({ id: formId, environmentName, appId: resolvedAppId })
|
|
239
|
+
.promise();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
printer.debug(err.toString());
|
|
242
|
+
throw err;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
233
246
|
getModels = async (resourceName: string, envName?: string, appId?: string): Promise<string | undefined> => {
|
|
234
247
|
try {
|
|
235
248
|
const environmentName = envName || this.#envName;
|
|
@@ -11,7 +11,7 @@ export const run = async (context: $TSContext): Promise<void> => {
|
|
|
11
11
|
const args = extractArgs(context);
|
|
12
12
|
const sourceEnvName = args.sourceEnvName ? args.sourceEnvName : context.exeInfo.sourceEnvName;
|
|
13
13
|
const newEnvName = args.newEnvName ? args.newEnvName : context.exeInfo.localEnvInfo.envName;
|
|
14
|
-
const appId = args.appId ?? context.exeInfo
|
|
14
|
+
const appId = args.appId ?? context.exeInfo?.teamProviderInfo?.[sourceEnvName]?.awscloudformation?.AmplifyAppId;
|
|
15
15
|
const studioClient = await AmplifyStudioClient.setClientInfo(context, sourceEnvName, appId);
|
|
16
16
|
|
|
17
17
|
const [existingComponents, existingComponentsNewEnv] = await Promise.all([
|
|
@@ -13,12 +13,15 @@ import {
|
|
|
13
13
|
generateAmplifyUiBuilderIndexFile,
|
|
14
14
|
generateUiBuilderForms,
|
|
15
15
|
generateAmplifyUiBuilderUtilFile,
|
|
16
|
+
isStudioForm,
|
|
17
|
+
isFormDetachedFromModel,
|
|
18
|
+
deleteDetachedForms,
|
|
16
19
|
} from './utils';
|
|
17
20
|
|
|
18
21
|
/**
|
|
19
22
|
* Pulls ui components from Studio backend and generates the code in the user's file system
|
|
20
23
|
*/
|
|
21
|
-
export const run = async (context: $TSContext): Promise<void> => {
|
|
24
|
+
export const run = async (context: $TSContext, eventType: 'PostPush' | 'PostPull'): Promise<void> => {
|
|
22
25
|
if (!(await shouldRenderComponents(context))) {
|
|
23
26
|
return;
|
|
24
27
|
}
|
|
@@ -32,9 +35,9 @@ export const run = async (context: $TSContext): Promise<void> => {
|
|
|
32
35
|
getAmplifyDataSchema(studioClient),
|
|
33
36
|
]);
|
|
34
37
|
|
|
35
|
-
const
|
|
38
|
+
const nothingWouldAutogenerate = !dataSchema || !studioClient.metadata.autoGenerateForms || !studioClient.isGraphQLSupported;
|
|
36
39
|
|
|
37
|
-
if (
|
|
40
|
+
if (nothingWouldAutogenerate && [componentSchemas, themeSchemas, formSchemas].every(group => !group.entities.length)) {
|
|
38
41
|
printer.debug('Skipping UI component generation since none are found.');
|
|
39
42
|
return;
|
|
40
43
|
}
|
|
@@ -44,13 +47,18 @@ export const run = async (context: $TSContext): Promise<void> => {
|
|
|
44
47
|
component: generateUiBuilderComponents(context, componentSchemas.entities, dataSchema),
|
|
45
48
|
theme: generateUiBuilderThemes(context, themeSchemas.entities),
|
|
46
49
|
form: generateUiBuilderForms(
|
|
47
|
-
context,
|
|
50
|
+
context,
|
|
51
|
+
formSchemas.entities,
|
|
52
|
+
dataSchema,
|
|
53
|
+
studioClient.metadata.autoGenerateForms && studioClient.isGraphQLSupported,
|
|
48
54
|
),
|
|
49
55
|
};
|
|
50
56
|
|
|
51
57
|
const successfulSchemas: StudioSchema[] = [];
|
|
58
|
+
const detachedForms: {id: string, name: string}[] = [];
|
|
52
59
|
let hasSuccessfulForm = false;
|
|
53
60
|
const failedResponseNames: string[] = [];
|
|
61
|
+
const modelNames = dataSchema?.models ? new Set(Object.keys(dataSchema.models)) : new Set<string>();
|
|
54
62
|
|
|
55
63
|
Object.entries(generatedResults).forEach(([key, results]) => {
|
|
56
64
|
results.forEach(result => {
|
|
@@ -60,6 +68,22 @@ export const run = async (context: $TSContext): Promise<void> => {
|
|
|
60
68
|
hasSuccessfulForm = true;
|
|
61
69
|
}
|
|
62
70
|
} else {
|
|
71
|
+
const failedSchema = result.schema;
|
|
72
|
+
/**
|
|
73
|
+
* A form resource may fail to generate because it's DataStore model type
|
|
74
|
+
* no longer exists.
|
|
75
|
+
*/
|
|
76
|
+
if (
|
|
77
|
+
isStudioForm(failedSchema)
|
|
78
|
+
&& failedSchema.id
|
|
79
|
+
&& dataSchema
|
|
80
|
+
&& eventType === 'PostPush'
|
|
81
|
+
&& isFormDetachedFromModel(failedSchema, modelNames)
|
|
82
|
+
) {
|
|
83
|
+
// Don't need to add form to failedResponseNames if it is going to be deleted
|
|
84
|
+
detachedForms.push({id: failedSchema.id, name: failedSchema.name});
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
63
87
|
failedResponseNames.push(result.schemaName);
|
|
64
88
|
}
|
|
65
89
|
});
|
|
@@ -88,6 +112,8 @@ export const run = async (context: $TSContext): Promise<void> => {
|
|
|
88
112
|
}
|
|
89
113
|
|
|
90
114
|
notifyMissingPackages(context);
|
|
115
|
+
|
|
116
|
+
deleteDetachedForms(detachedForms, studioClient);
|
|
91
117
|
} catch (e) {
|
|
92
118
|
printer.debug(e);
|
|
93
119
|
spinner.fail('Failed to sync UI components');
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { StudioForm, StudioSchema } from '@aws-amplify/codegen-ui';
|
|
2
|
+
import { Form } from 'aws-sdk/clients/amplifyuibuilder';
|
|
3
|
+
import { AmplifyStudioClient } from '../../clients';
|
|
4
|
+
import { printer } from 'amplify-prompts';
|
|
5
|
+
import asyncPool from 'tiny-async-pool';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Does the form's reference DataStore type exist in the list of models
|
|
9
|
+
*/
|
|
10
|
+
export const isFormDetachedFromModel = (formSchema: StudioForm | Form, modelNames: Set<string>): boolean => {
|
|
11
|
+
return formSchema.dataType.dataSourceType === 'DataStore' && !modelNames.has(formSchema.dataType.dataTypeName);
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* A form schema will be customized if fields, style, sectionalElements objects aren't empty
|
|
16
|
+
* @param schema The schema to test
|
|
17
|
+
* @returns boolean
|
|
18
|
+
*/
|
|
19
|
+
export const isFormSchemaCustomized = (formSchema: StudioForm | Form): boolean => {
|
|
20
|
+
const { fields, style, sectionalElements } = formSchema;
|
|
21
|
+
// Fields and sectionalElements just need an empty check
|
|
22
|
+
if (!isEmpty({ ...fields, ...sectionalElements })) {
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check style
|
|
27
|
+
// If not empty, at least one style must be defined
|
|
28
|
+
return Object.values(style).some(styleConfig => styleConfig !== undefined);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Is the schema a StudioForm
|
|
33
|
+
*/
|
|
34
|
+
export const isStudioForm = (schema: StudioSchema | undefined): schema is StudioForm => {
|
|
35
|
+
if (!schema) return false;
|
|
36
|
+
return 'formActionType' in schema;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const deleteDetachedForms = async (detachedForms: {id: string, name: string}[], studioClient: AmplifyStudioClient) => {
|
|
40
|
+
const deleteForm = async ({id, name}: {id: string, name: string}) => {
|
|
41
|
+
try {
|
|
42
|
+
// Try to delete the form but don't stop any other operations if it fails
|
|
43
|
+
await studioClient.deleteForm(id);
|
|
44
|
+
return {status: 'SUCCESS', message: `Deleted detached form ${name}`};
|
|
45
|
+
} catch (error) {
|
|
46
|
+
return {status: 'FAIL', message: `Failed to delete detached form ${name}`}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
for await (const status of asyncPool(5, detachedForms, deleteForm)) {
|
|
50
|
+
printer.debug(status.message);
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
55
|
+
const isEmpty = (obj?: any): boolean => Object.keys(obj).length === 0;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable prefer-arrow/prefer-arrow-functions */
|
|
2
|
-
import { $TSContext } from 'amplify-cli-core';
|
|
2
|
+
import { $TSContext, stateManager } from 'amplify-cli-core';
|
|
3
3
|
import { printer } from 'amplify-prompts';
|
|
4
4
|
import { AmplifyStudioClient } from '../../clients';
|
|
5
5
|
import { getAppId } from './environmentHelpers';
|
|
@@ -8,6 +8,8 @@ import { getAppId } from './environmentHelpers';
|
|
|
8
8
|
* process to decide if we should render components
|
|
9
9
|
*/
|
|
10
10
|
export const shouldRenderComponents = async (context: $TSContext): Promise<boolean> => {
|
|
11
|
+
const projectConfig = stateManager.getProjectConfig();
|
|
12
|
+
|
|
11
13
|
if (process.env.FORCE_RENDER) {
|
|
12
14
|
printer.debug('Forcing component render since environment variable flag is set.');
|
|
13
15
|
return true;
|
|
@@ -17,22 +19,22 @@ export const shouldRenderComponents = async (context: $TSContext): Promise<boole
|
|
|
17
19
|
return false;
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
if (!
|
|
22
|
+
if (!projectConfig) {
|
|
21
23
|
printer.debug('Not pulling components because there is no projectConfig set for this project.');
|
|
22
24
|
return false;
|
|
23
25
|
}
|
|
24
26
|
|
|
25
|
-
if (!
|
|
27
|
+
if (!projectConfig.providers.includes('awscloudformation')) {
|
|
26
28
|
printer.debug('Not pulling components because there is no "awscloudformation" provider.');
|
|
27
29
|
return false;
|
|
28
30
|
}
|
|
29
31
|
|
|
30
|
-
if (
|
|
32
|
+
if (projectConfig.frontend !== 'javascript') {
|
|
31
33
|
printer.debug('Not pulling components because this project is not configured as a javascript frontend.');
|
|
32
34
|
return false;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
if (
|
|
37
|
+
if (projectConfig.javascript.framework !== 'react') {
|
|
36
38
|
printer.debug('Not pulling components because this project is not configured with the "react" framework.');
|
|
37
39
|
return false;
|
|
38
40
|
}
|
|
@@ -120,7 +120,9 @@ export const generateUiBuilderForms = (
|
|
|
120
120
|
} catch (e) {
|
|
121
121
|
printer.debug(`Failure caught processing ${schema.name}`);
|
|
122
122
|
printer.debug(e);
|
|
123
|
-
return {
|
|
123
|
+
return {
|
|
124
|
+
resultType: 'FAILURE', schemaName: schema.name, schema, error: e,
|
|
125
|
+
};
|
|
124
126
|
}
|
|
125
127
|
};
|
|
126
128
|
const formResults = formSchemas.map((schema: StudioForm) => {
|
|
@@ -7,7 +7,7 @@ import { run as generateComponents } from '../commands/generateComponents';
|
|
|
7
7
|
*/
|
|
8
8
|
export const run = async (context: $TSContext): Promise<void> => {
|
|
9
9
|
try {
|
|
10
|
-
await generateComponents(context);
|
|
10
|
+
await generateComponents(context, 'PostPull');
|
|
11
11
|
} catch (e) {
|
|
12
12
|
// Swallow all errors from the uibuilder plugin
|
|
13
13
|
printer.debug(e);
|
|
@@ -7,7 +7,7 @@ import { run as generateComponents } from '../commands/generateComponents';
|
|
|
7
7
|
*/
|
|
8
8
|
export const run = async (context: $TSContext): Promise<void> => {
|
|
9
9
|
try {
|
|
10
|
-
await generateComponents(context);
|
|
10
|
+
await generateComponents(context, 'PostPush');
|
|
11
11
|
} catch (e) {
|
|
12
12
|
// Swallow all errors from the uibuilder plugin
|
|
13
13
|
printer.debug(e);
|