diff --git a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-views.test.ts.snap b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-views.test.ts.snap
index 0cfe0c320..8608d9deb 100644
--- a/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-views.test.ts.snap
+++ b/packages/codegen-ui-react/lib/__tests__/__snapshots__/studio-ui-codegen-react-views.test.ts.snap
@@ -1,5 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`amplify expander renderer tests should generate expander with an expander item that has a component as a child 1`] = `
+"
+ {items.map((item) => (
+
+
+
+ ))}
+;
+"
+`;
+
+exports[`amplify expander renderer tests should generate expander with an expander item that has a data binding as a child 1`] = `
+"
+ {items.map((item) => (
+
+ {item.questionAnswer}
+
+ ))}
+;
+"
+`;
+
exports[`amplify table renderer tests should generate a non-datastore table element 1`] = `
"
{!disableHeaders && (
@@ -90,6 +115,7 @@ exports[`amplify view renderer tests should call util file if rendered 1`] = `
"/* eslint-disable */
import * as React from \\"react\\";
import { formatter } from \\"./utils\\";
+import { Post } from \\"../models\\";
import {
createDataStorePredicate,
useDataStoreBinding,
@@ -104,7 +130,7 @@ import {
} from \\"@aws-amplify/ui-react\\";
export default function MyPostTable(props) {
const {
- items: itemsProps,
+ items: itemsProp,
predicateOverride,
formatOverride,
highlightOnHover,
@@ -120,13 +146,13 @@ export default function MyPostTable(props) {
};
const postPredicate = createDataStorePredicate(postFilter);
const postPagination = { sort: (s) => s.username(SortDirection.ASCENDING) };
- const MyPostTableDataStore = useDataStoreBinding({
+ const postDataStore = useDataStoreBinding({
type: \\"collection\\",
model: Post,
criteria: predicateOverride || postPredicate,
pagination: postPagination,
}).items;
- const items = itemsProp !== undefined ? itemsProp : MyPostTableDataStore;
+ const items = itemsProp !== undefined ? itemsProp : postDataStore;
return (
{!disableHeaders && (
@@ -192,9 +218,10 @@ export default function MyPostTable(props) {
exports[`amplify view renderer tests should call util file if rendered 2`] = `
"import * as React from \\"react\\";
-import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\";
+import { EscapeHatchProps, createDataStorePredicate } from \\"@aws-amplify/ui-react/internal\\";
export declare type MyPostTableProps = React.PropsWithChildren<{
overrides?: EscapeHatchProps | undefined | null;
+ predicateOverride?: ReturnType | undefined | null;
}>;
export default function MyPostTable(props: MyPostTableProps): React.ReactElement;
"
@@ -259,9 +286,10 @@ export default function CustomTable(props) {
exports[`amplify view renderer tests should render view with custom datastore 2`] = `
"import * as React from \\"react\\";
-import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\";
+import { EscapeHatchProps, createDataStorePredicate } from \\"@aws-amplify/ui-react/internal\\";
export declare type CustomTableProps = React.PropsWithChildren<{
overrides?: EscapeHatchProps | undefined | null;
+ predicateOverride?: ReturnType | undefined | null;
}>;
export default function CustomTable(props: CustomTableProps): React.ReactElement;
"
@@ -270,6 +298,7 @@ export default function CustomTable(props: CustomTableProps): React.ReactElement
exports[`amplify view renderer tests should render view with passed in predicate and sort 1`] = `
"/* eslint-disable */
import * as React from \\"react\\";
+import { Post } from \\"../models\\";
import {
createDataStorePredicate,
useDataStoreBinding,
@@ -284,7 +313,7 @@ import {
} from \\"@aws-amplify/ui-react\\";
export default function MyPostTable(props) {
const {
- items: itemsProps,
+ items: itemsProp,
predicateOverride,
formatOverride,
highlightOnHover,
@@ -300,13 +329,13 @@ export default function MyPostTable(props) {
};
const postPredicate = createDataStorePredicate(postFilter);
const postPagination = { sort: (s) => s.username(SortDirection.ASCENDING) };
- const MyPostTableDataStore = useDataStoreBinding({
+ const postDataStore = useDataStoreBinding({
type: \\"collection\\",
model: Post,
criteria: predicateOverride || postPredicate,
pagination: postPagination,
}).items;
- const items = itemsProp !== undefined ? itemsProp : MyPostTableDataStore;
+ const items = itemsProp !== undefined ? itemsProp : postDataStore;
return (
{!disableHeaders && (
@@ -369,9 +398,10 @@ export default function MyPostTable(props) {
exports[`amplify view renderer tests should render view with passed in predicate and sort 2`] = `
"import * as React from \\"react\\";
-import { EscapeHatchProps } from \\"@aws-amplify/ui-react/internal\\";
+import { EscapeHatchProps, createDataStorePredicate } from \\"@aws-amplify/ui-react/internal\\";
export declare type MyPostTableProps = React.PropsWithChildren<{
overrides?: EscapeHatchProps | undefined | null;
+ predicateOverride?: ReturnType | undefined | null;
}>;
export default function MyPostTable(props: MyPostTableProps): React.ReactElement;
"
diff --git a/packages/codegen-ui-react/lib/__tests__/__utils__/amplify-renderer-generator.ts b/packages/codegen-ui-react/lib/__tests__/__utils__/amplify-renderer-generator.ts
index 6a2e67286..624274a8b 100644
--- a/packages/codegen-ui-react/lib/__tests__/__utils__/amplify-renderer-generator.ts
+++ b/packages/codegen-ui-react/lib/__tests__/__utils__/amplify-renderer-generator.ts
@@ -118,3 +118,20 @@ export const genericPrinter = (node: Node): string => {
});
return printer.printNode(EmitHint.Unspecified, node, file);
};
+
+export const renderExpanderJsxElement = (
+ filePath: string,
+ dataSchemaFilePath: string | undefined,
+ snapshotFileName: string,
+ renderConfig: ReactRenderConfig = defaultCLIRenderConfig,
+): string => {
+ const expander = loadSchemaFromJSONFile(filePath);
+ const dataSchema = dataSchemaFilePath ? loadSchemaFromJSONFile(dataSchemaFilePath) : undefined;
+ const expanderJsx = new AmplifyViewRenderer(expander, dataSchema, renderConfig).renderJsx();
+
+ const file = createSourceFile(snapshotFileName, '', ScriptTarget.ES2015, true, ScriptKind.TS);
+ const printer = createPrinter();
+ const expanderNode = printer.printNode(EmitHint.Unspecified, expanderJsx, file);
+
+ return transpile(expanderNode, {}).componentText;
+};
diff --git a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-views.test.ts b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-views.test.ts
index ac36b8c38..e4fcf8f04 100644
--- a/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-views.test.ts
+++ b/packages/codegen-ui-react/lib/__tests__/studio-ui-codegen-react-views.test.ts
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-import { renderTableJsxElement, renderWithAmplifyViewRenderer } from './__utils__';
+import { renderExpanderJsxElement, renderTableJsxElement, renderWithAmplifyViewRenderer } from './__utils__';
describe('amplify table renderer tests', () => {
test('should generate a table element', () => {
@@ -27,6 +27,26 @@ describe('amplify table renderer tests', () => {
});
});
+describe('amplify expander renderer tests', () => {
+ test('should generate expander with an expander item that has a component as a child', () => {
+ const expander = renderExpanderJsxElement(
+ 'views/expander-with-component-slot',
+ undefined,
+ 'test-expander-component-slot.ts',
+ );
+ expect(expander).toMatchSnapshot();
+ });
+
+ test('should generate expander with an expander item that has a data binding as a child', () => {
+ const expander = renderExpanderJsxElement(
+ 'views/expander-with-binding-prop',
+ undefined,
+ 'test-expander-binding-prop.ts',
+ );
+ expect(expander).toMatchSnapshot();
+ });
+});
+
describe('amplify view renderer tests', () => {
test('should render view with passed in predicate and sort', () => {
const { componentText, declaration } = renderWithAmplifyViewRenderer(
diff --git a/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-view-renderer.ts b/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-view-renderer.ts
index eed37e3cf..93c6d019d 100644
--- a/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-view-renderer.ts
+++ b/packages/codegen-ui-react/lib/amplify-ui-renderers/amplify-view-renderer.ts
@@ -18,6 +18,7 @@ import { JsxElement, factory, JsxFragment } from 'typescript';
import { ReactViewTemplateRenderer } from '../views/react-view-renderer';
import { Primitive } from '../primitive';
import { ReactTableRenderer } from '../react-table-renderer';
+import { ReactExpanderRenderer } from '../react-expander-renderer';
export class AmplifyViewRenderer extends ReactViewTemplateRenderer {
renderJsx(): JsxElement | JsxFragment {
@@ -29,6 +30,15 @@ export class AmplifyViewRenderer extends ReactViewTemplateRenderer {
this.viewMetadata,
this.importCollection,
).renderElement();
+ case 'Collection':
+ switch (this.viewComponent.viewConfiguration.collection.collectionType) {
+ case 'expander':
+ return new ReactExpanderRenderer(this.viewComponent, this.importCollection).renderElement();
+ default:
+ throw new Error(
+ `Collection type ${this.viewComponent.viewConfiguration.collection.collectionType} is not supported`,
+ );
+ }
default:
return factory.createJsxFragment(factory.createJsxOpeningFragment(), [], factory.createJsxJsxClosingFragment());
}
diff --git a/packages/codegen-ui-react/lib/index.ts b/packages/codegen-ui-react/lib/index.ts
index 25fd4db47..2b978dbed 100644
--- a/packages/codegen-ui-react/lib/index.ts
+++ b/packages/codegen-ui-react/lib/index.ts
@@ -14,6 +14,7 @@
limitations under the License.
*/
export * from './react-component-renderer';
+export * from './react-expander-renderer';
export * from './react-table-renderer';
export * from './imports';
export * from './react-studio-template-renderer';
diff --git a/packages/codegen-ui-react/lib/react-expander-renderer.ts b/packages/codegen-ui-react/lib/react-expander-renderer.ts
new file mode 100644
index 000000000..bc6079612
--- /dev/null
+++ b/packages/codegen-ui-react/lib/react-expander-renderer.ts
@@ -0,0 +1,204 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License").
+ You may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+import { StudioView } from '@aws-amplify/codegen-ui/lib/types';
+import { factory, JsxChild, JsxElement, SyntaxKind } from 'typescript';
+import { ImportCollection, ImportSource } from './imports';
+import { Primitive } from './primitive';
+
+export class ReactExpanderRenderer {
+ private requiredUIReactImports = [Primitive.Expander, Primitive.ExpanderItem];
+
+ protected expander: StudioView;
+
+ constructor(expander: StudioView, private imports: ImportCollection) {
+ this.expander = expander;
+
+ this.requiredUIReactImports.forEach((importName) => {
+ imports.addImport(ImportSource.UI_REACT, importName);
+ });
+ }
+
+ renderElement(): JsxElement {
+ return factory.createJsxElement(
+ this.createOpeningExpanderElement(),
+ [this.createExpanderChildren()],
+ factory.createJsxClosingElement(factory.createIdentifier(Primitive.Expander)),
+ );
+ }
+
+ createExpanderChildren() {
+ const itemParam = factory.createParameterDeclaration(
+ undefined,
+ undefined,
+ undefined,
+ factory.createIdentifier('item'),
+ undefined,
+ undefined,
+ undefined,
+ );
+ return factory.createJsxExpression(
+ undefined,
+ factory.createCallExpression(
+ factory.createPropertyAccessExpression(factory.createIdentifier('items'), factory.createIdentifier('map')),
+ undefined,
+ [
+ factory.createArrowFunction(
+ undefined,
+ undefined,
+ [itemParam],
+ undefined,
+ factory.createToken(SyntaxKind.EqualsGreaterThanToken),
+ factory.createParenthesizedExpression(this.createExpanderItemRow()),
+ ),
+ ],
+ ),
+ );
+ }
+
+ createExpanderItemRow() {
+ const child = this.createExpanderItemChild();
+ if (!child) throw new Error('could not determine ExpanderItem child to render');
+ return factory.createJsxElement(
+ factory.createJsxOpeningElement(
+ factory.createIdentifier(Primitive.ExpanderItem),
+ undefined,
+ this.createExpanderItemAttributes(),
+ ),
+ [child],
+ factory.createJsxClosingElement(factory.createIdentifier(Primitive.ExpanderItem)),
+ );
+ }
+
+ createExpanderItemChild(): JsxChild | undefined {
+ const { viewConfiguration } = this.expander;
+ if (viewConfiguration.type !== 'Collection') {
+ throw new Error(`Cannot create ExpanderItem child for view type: ${viewConfiguration.type}`);
+ }
+ switch (viewConfiguration.collection.body.type) {
+ case 'Amplify.ComponentSlot': {
+ if (!viewConfiguration.collection.body.content.componentSlot.componentName) {
+ throw new Error('componentName must be defined');
+ }
+ const { componentSlot } = viewConfiguration.collection.body.content;
+ this.imports.addImport(ImportSource.UI_REACT, componentSlot.componentName);
+
+ const attributes = Object.entries(componentSlot.bindingProperties).map(([key, value]) => {
+ const attributeValue = value.field
+ ? factory.createPropertyAccessExpression(
+ factory.createIdentifier(value.property),
+ factory.createIdentifier(value.field),
+ )
+ : factory.createIdentifier(value.property);
+
+ return factory.createJsxAttribute(
+ factory.createIdentifier(key),
+ factory.createJsxExpression(undefined, attributeValue),
+ );
+ });
+
+ return factory.createJsxSelfClosingElement(
+ factory.createIdentifier(viewConfiguration.collection.body.content.componentSlot.componentName),
+ undefined,
+ factory.createJsxAttributes(attributes),
+ );
+ }
+ case 'Amplify.Binding': {
+ if (!viewConfiguration.collection.body.content.bindingProperty) {
+ throw new Error('bindingProperty must be defined');
+ }
+ const { property, field } = viewConfiguration.collection.body.content.bindingProperty;
+ if (!(property && field)) {
+ throw new Error('property and field must be defined');
+ }
+ return factory.createJsxExpression(
+ undefined,
+ factory.createPropertyAccessExpression(factory.createIdentifier(property), factory.createIdentifier(field)),
+ );
+ }
+ default: {
+ // "viewConfiguration.collection.body.type" should be a "never" TS type here, so we can't reference it.
+ throw new Error(`Unrecognized value for field type in viewConfiguration.collection.body`);
+ }
+ }
+ }
+
+ createExpanderItemAttributes() {
+ const { viewConfiguration } = this.expander;
+ if (viewConfiguration.type === 'Collection') {
+ switch (viewConfiguration.collection.title?.type) {
+ case 'Amplify.Binding':
+ return factory.createJsxAttributes([
+ factory.createJsxAttribute(
+ factory.createIdentifier('title'),
+ factory.createJsxExpression(
+ undefined,
+ factory.createPropertyAccessExpression(
+ /**
+ * leaving "item" hard coded since it's controlled by the code in this package
+ * and it should never be something else in this context,
+ * but the user can still set this in the schema.
+ */
+ factory.createIdentifier('item'),
+ factory.createIdentifier(viewConfiguration.collection.title.content.bindingProperty.field),
+ ),
+ ),
+ ),
+ /**
+ * Value is needed for the expander item to expand,
+ * the user doesn't need it
+ */
+ factory.createJsxAttribute(
+ factory.createIdentifier('value'),
+ factory.createJsxExpression(
+ undefined,
+ factory.createPropertyAccessExpression(
+ factory.createIdentifier('item'),
+ factory.createIdentifier('id'),
+ ),
+ ),
+ ),
+ /**
+ * Key here is for React to render a list of items
+ */
+ factory.createJsxAttribute(
+ factory.createIdentifier('key'),
+ factory.createJsxExpression(
+ undefined,
+ factory.createPropertyAccessExpression(
+ factory.createIdentifier('item'),
+ factory.createIdentifier('id'),
+ ),
+ ),
+ ),
+ ]);
+ default:
+ throw new Error(`Unsupported Expander title type, ${viewConfiguration.collection.title.type}`);
+ }
+ }
+ return factory.createJsxAttributes([]);
+ }
+
+ createOpeningExpanderElement() {
+ return factory.createJsxOpeningElement(
+ factory.createIdentifier(Primitive.Expander),
+ undefined,
+ // TODO: updating "type" here is handled in a future task
+ factory.createJsxAttributes([
+ factory.createJsxAttribute(factory.createIdentifier('type'), factory.createStringLiteral('multiple')),
+ ]),
+ );
+ }
+}
diff --git a/packages/codegen-ui-react/lib/react-table-renderer-helper.ts b/packages/codegen-ui-react/lib/react-table-renderer-helper.ts
index b52ea9d17..fc5b221c0 100644
--- a/packages/codegen-ui-react/lib/react-table-renderer-helper.ts
+++ b/packages/codegen-ui-react/lib/react-table-renderer-helper.ts
@@ -19,6 +19,7 @@ import { CallExpression, factory, ObjectLiteralExpression, SyntaxKind } from 'ty
export const getFilterName = (model: string) => `${model.toLowerCase()}Filter`;
export const getPredicateName = (model: string) => `${model.toLowerCase()}Predicate`;
export const getPaginationName = (model: string) => `${model.toLowerCase()}Pagination`;
+export const getDataStoreName = (model: string) => `${model.toLowerCase()}DataStore`;
/*
checks table to see if there is a formatter for stringFormat
diff --git a/packages/codegen-ui-react/lib/views/react-view-renderer.ts b/packages/codegen-ui-react/lib/views/react-view-renderer.ts
index 7b1a482f0..f5e9e4a34 100644
--- a/packages/codegen-ui-react/lib/views/react-view-renderer.ts
+++ b/packages/codegen-ui-react/lib/views/react-view-renderer.ts
@@ -25,6 +25,7 @@ import {
handleCodegenErrors,
validateViewSchema,
StudioComponentPredicate,
+ DEFAULT_TABLE_DEFINITION,
} from '@aws-amplify/codegen-ui';
import {
addSyntheticLeadingComment,
@@ -60,6 +61,7 @@ import { ReactRenderConfig, scriptKindToFileExtension } from '../react-render-co
import { RequiredKeys } from '../utils/type-utils';
import {
buildDataStoreCollectionCall,
+ getDataStoreName,
getFilterName,
getPaginationName,
getPredicateName,
@@ -79,7 +81,7 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer<
protected renderConfig: RequiredKeys;
- protected viewDefinition: TableDefinition;
+ protected viewDefinition: TableDefinition = DEFAULT_TABLE_DEFINITION;
protected viewComponent: StudioView;
@@ -101,18 +103,20 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer<
switch (component.viewConfiguration.type) {
case 'Table':
this.viewDefinition = generateTableDefinition(component, dataSchema);
+ // find if formatter is required
+ if (needsFormatter(component.viewConfiguration)) {
+ this.importCollection.addMappedImport(ImportValue.FORMATTER);
+ }
+ break;
+ case 'Collection':
+ // 'Collection' type doesn't need a viewDefinition
break;
default:
- throw new Error(`Type: ${component.viewConfiguration.type} is not supported.`);
+ throw new Error(`Encountered a viewConfiguration type that is not supported.`);
}
this.viewComponent = component;
- // find if formatter is required
- if (needsFormatter(component.viewConfiguration)) {
- this.importCollection.addMappedImport(ImportValue.FORMATTER);
- }
-
this.viewMetadata = {
id: component.id,
name: component.name,
@@ -252,15 +256,16 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer<
const statements: Statement[] = [];
const elements: BindingElement[] = [];
const { type, model, predicate, sort } = this.viewComponent.dataSource;
+ const itemsProp = 'itemsProp';
const isDataStoreEnabled = type === 'DataStore' && model;
if (isDataStoreEnabled) {
- this.importCollection.addImport(ImportSource.LOCAL_MODELS, this.component.dataSource.type);
+ this.importCollection.addImport(ImportSource.LOCAL_MODELS, model);
this.importCollection.addMappedImport(ImportValue.USE_DATA_STORE_BINDING);
elements.push(
factory.createBindingElement(
undefined,
factory.createIdentifier('items'),
- factory.createIdentifier('itemsProps'),
+ factory.createIdentifier(itemsProp),
undefined,
),
factory.createBindingElement(undefined, undefined, factory.createIdentifier('predicateOverride'), undefined),
@@ -272,13 +277,15 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer<
// add base Props
// props
- const props = [
- factory.createBindingElement(undefined, undefined, factory.createIdentifier('formatOverride'), undefined),
- factory.createBindingElement(undefined, undefined, factory.createIdentifier('highlightOnHover'), undefined),
- factory.createBindingElement(undefined, undefined, factory.createIdentifier('onRowClick'), undefined),
- factory.createBindingElement(undefined, undefined, factory.createIdentifier('disableHeaders'), undefined),
- ];
- elements.push(...props);
+ if (this.viewComponent.viewConfiguration.type === 'Table') {
+ const props = [
+ factory.createBindingElement(undefined, undefined, factory.createIdentifier('formatOverride'), undefined),
+ factory.createBindingElement(undefined, undefined, factory.createIdentifier('highlightOnHover'), undefined),
+ factory.createBindingElement(undefined, undefined, factory.createIdentifier('onRowClick'), undefined),
+ factory.createBindingElement(undefined, undefined, factory.createIdentifier('disableHeaders'), undefined),
+ ];
+ elements.push(...props);
+ }
// get rest of props to pass to top level component
elements.push(
@@ -385,7 +392,7 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer<
if custom enabled
uses regular items array for formatting
*/
- const dsItemsName = factory.createIdentifier(`${this.viewComponent.name}DataStore`);
+ const dsItemsName = factory.createIdentifier(getDataStoreName(model));
statements.push(
buildBaseCollectionVariableStatement(
dsItemsName,
@@ -406,12 +413,12 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer<
undefined,
factory.createConditionalExpression(
factory.createBinaryExpression(
- factory.createIdentifier('itemsProp'),
+ factory.createIdentifier(itemsProp),
factory.createToken(SyntaxKind.ExclamationEqualsEqualsToken),
factory.createIdentifier('undefined'),
),
factory.createToken(SyntaxKind.QuestionToken),
- factory.createIdentifier('itemsProp'),
+ factory.createIdentifier(itemsProp),
factory.createToken(SyntaxKind.ColonToken),
dsItemsName,
),
@@ -455,10 +462,23 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer<
factory.createLiteralTypeNode(factory.createNull()),
]),
),
+ factory.createPropertySignature(
+ undefined,
+ factory.createIdentifier('predicateOverride'),
+ factory.createToken(SyntaxKind.QuestionToken),
+ factory.createUnionTypeNode([
+ factory.createTypeReferenceNode(factory.createIdentifier('ReturnType'), [
+ factory.createTypeQueryNode(factory.createIdentifier('createDataStorePredicate')),
+ ]),
+ factory.createKeywordTypeNode(SyntaxKind.UndefinedKeyword),
+ factory.createLiteralTypeNode(factory.createNull()),
+ ]),
+ ),
]);
const formPropType = getComponentPropName(this.component.name);
this.importCollection.addMappedImport(ImportValue.ESCAPE_HATCH_PROPS);
+ this.importCollection.addMappedImport(ImportValue.CREATE_DATA_STORE_PREDICATE);
return [
factory.createTypeAliasDeclaration(
diff --git a/packages/codegen-ui/example-schemas/views/expander-with-binding-prop.json b/packages/codegen-ui/example-schemas/views/expander-with-binding-prop.json
new file mode 100644
index 000000000..56bd088b5
--- /dev/null
+++ b/packages/codegen-ui/example-schemas/views/expander-with-binding-prop.json
@@ -0,0 +1,47 @@
+{
+ "appId": "d1234",
+ "dataSource": {
+ "model": "Faq",
+ "predicate": {
+ "field": "enabled",
+ "operand": "true",
+ "operator": "eq"
+ },
+ "sort": [
+ {
+ "field": "updatedAt",
+ "direction": "DESC"
+ }
+ ],
+ "type": "DataStore"
+ },
+ "environmentName": "staging",
+ "id": "v-123456",
+ "name": "FaqExpander",
+ "schemaVersion": "1.0",
+ "style": {},
+ "viewConfiguration": {
+ "type": "Collection",
+ "collection": {
+ "collectionType": "expander",
+ "title": {
+ "type": "Amplify.Binding",
+ "content": {
+ "bindingProperty": {
+ "property": "item",
+ "field": "questionTitle"
+ }
+ }
+ },
+ "body": {
+ "type": "Amplify.Binding",
+ "content": {
+ "bindingProperty": {
+ "property": "item",
+ "field": "questionAnswer"
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/codegen-ui/example-schemas/views/expander-with-component-slot.json b/packages/codegen-ui/example-schemas/views/expander-with-component-slot.json
new file mode 100644
index 000000000..367576966
--- /dev/null
+++ b/packages/codegen-ui/example-schemas/views/expander-with-component-slot.json
@@ -0,0 +1,56 @@
+{
+ "appId": "d1234",
+ "dataSource": {
+ "model": "Faq",
+ "predicate": {
+ "field": "enabled",
+ "operand": "true",
+ "operator": "eq"
+ },
+ "sort": [
+ {
+ "field": "updatedAt",
+ "direction": "DESC"
+ }
+ ],
+ "type": "DataStore"
+ },
+ "environmentName": "staging",
+ "id": "v-123456",
+ "name": "FaqExpander",
+ "schemaVersion": "1.0",
+ "style": {},
+ "viewConfiguration": {
+ "type": "Collection",
+ "collection": {
+ "collectionType": "expander",
+ "title": {
+ "type": "Amplify.Binding",
+ "content": {
+ "bindingProperty": {
+ "property": "item",
+ "field": "questionTitle"
+ }
+ }
+ },
+ "body": {
+ "type": "Amplify.ComponentSlot",
+ "content": {
+ "componentSlot": {
+ "componentName": "Ampligram",
+ "bindingProperties": {
+ "title": {
+ "property": "item",
+ "field": "answerTitle"
+ },
+ "description": {
+ "property": "item",
+ "field": "answerDescription"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/packages/codegen-ui/lib/__tests__/validation-helper.test.ts b/packages/codegen-ui/lib/__tests__/validation-helper.test.ts
index 4f635d80c..2b3975bd6 100644
--- a/packages/codegen-ui/lib/__tests__/validation-helper.test.ts
+++ b/packages/codegen-ui/lib/__tests__/validation-helper.test.ts
@@ -431,5 +431,7 @@ describe('validation-helper', () => {
});
}).toThrowErrorMatchingSnapshot();
});
+
+ // TODO: add tests for validateFormSchema and validateViewSchema
});
});
diff --git a/packages/codegen-ui/lib/generate-view-definition/generate-table-definition.ts b/packages/codegen-ui/lib/generate-view-definition/generate-table-definition.ts
index 40d0bad0d..4609c9783 100644
--- a/packages/codegen-ui/lib/generate-view-definition/generate-table-definition.ts
+++ b/packages/codegen-ui/lib/generate-view-definition/generate-table-definition.ts
@@ -34,6 +34,9 @@ import { orderAndFilterVisibleColumns } from './helpers';
* @returns a definition that translates to rendered JSX elements.
*/
export function generateTableDefinition(table: StudioView, dataSchema?: GenericDataSchema): TableDefinition {
+ if (table.viewConfiguration.type !== 'Table') {
+ throw new Error(`Cannot generate a Table definition for viewConfiguration type ${table.viewConfiguration.type}`);
+ }
const definition = DEFAULT_TABLE_DEFINITION;
definition.tableStyle = {
diff --git a/packages/codegen-ui/lib/types/view/collection.ts b/packages/codegen-ui/lib/types/view/collection.ts
new file mode 100644
index 000000000..935902f3b
--- /dev/null
+++ b/packages/codegen-ui/lib/types/view/collection.ts
@@ -0,0 +1,51 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License").
+ You may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+export interface CollectionBindingProperty {
+ property: string;
+ field: string;
+}
+
+export interface CollectionBodyComponentSlot {
+ componentName: string;
+ bindingProperties: {
+ [propertyName: string]: CollectionBindingProperty;
+ };
+}
+
+export interface CollectionBodyContent {
+ bindingProperty?: CollectionBindingProperty;
+ componentSlot?: CollectionBodyComponentSlot;
+}
+
+export interface CollectionBindingTitle {
+ type: 'Amplify.Binding';
+ content: {
+ bindingProperty: CollectionBindingProperty;
+ };
+}
+
+export interface CollectionComponentSlotBody {
+ type: 'Amplify.ComponentSlot';
+ content: {
+ componentSlot: CollectionBodyComponentSlot;
+ };
+}
+export interface CollectionBindingBody {
+ type: 'Amplify.Binding';
+ content: {
+ bindingProperty: CollectionBindingProperty;
+ };
+}
diff --git a/packages/codegen-ui/lib/types/view/view.ts b/packages/codegen-ui/lib/types/view/view.ts
index b0027ee01..65e52b98f 100644
--- a/packages/codegen-ui/lib/types/view/view.ts
+++ b/packages/codegen-ui/lib/types/view/view.ts
@@ -14,6 +14,7 @@
limitations under the License.
*/
import { StudioComponentPredicate, StudioComponentSort } from '../bindings';
+import { CollectionBindingBody, CollectionComponentSlotBody, CollectionBindingTitle } from './collection';
import { ViewStyle } from './style';
import { ColumnsMap } from './table';
@@ -29,6 +30,8 @@ export interface StudioView {
viewConfiguration: ViewConfiguration;
}
+export declare type ViewType = 'Table' | 'Collection';
+
export interface BaseViewConfiguration {
type: ViewType;
}
@@ -42,9 +45,17 @@ export interface TableConfiguration extends BaseViewConfiguration {
enableOnRowClick?: boolean;
};
}
+export interface CollectionConfiguration extends BaseViewConfiguration {
+ type: 'Collection';
+ collection: {
+ body: CollectionComponentSlotBody | CollectionBindingBody;
+ collectionType: 'expander';
+ title: CollectionBindingTitle;
+ };
+}
// Append other configuration types here
-export type ViewConfiguration = TableConfiguration;
+export type ViewConfiguration = TableConfiguration | CollectionConfiguration | CollectionConfiguration;
export interface ViewDataTypeConfig {
identifiers?: string[];
@@ -66,5 +77,3 @@ export interface ViewSummary {
}
export declare type ViewSummaryList = ViewSummary[];
-
-export declare type ViewType = 'Table';
diff --git a/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts b/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts
index dbd961c07..590dbde9a 100644
--- a/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts
+++ b/packages/test-generator/integration-test-templates/cypress/e2e/generate-spec.cy.ts
@@ -28,6 +28,7 @@ const EXPECTED_SUCCESSFUL_CASES = new Set([
'BasicComponentImage',
'BasicComponentText',
'BasicComponentCustomRating',
+ 'ListingExpanderWithComponentSlot',
'CustomFormCreateDog',
'DataStoreFormCreateAllSupportedFormFields',
'ComponentWithDataBindingWithPredicate',
diff --git a/packages/test-generator/integration-test-templates/cypress/e2e/views-spec.cy.ts b/packages/test-generator/integration-test-templates/cypress/e2e/views-spec.cy.ts
new file mode 100644
index 000000000..bf346698f
--- /dev/null
+++ b/packages/test-generator/integration-test-templates/cypress/e2e/views-spec.cy.ts
@@ -0,0 +1,37 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License").
+ You may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+describe('Expander', () => {
+ before(() => {
+ cy.visit('http://localhost:3000/view-tests');
+ });
+
+ it('renders expander with component slot', () => {
+ cy.get('#expanderWithSlot').within(() => {
+ const description = 'ut labore et dolore magna aliqua. Ut enim ad minim veniam';
+ cy.contains(description).should('not.exist');
+ // Open expander item to show description
+ cy.contains('Quiet Cottage').click();
+ cy.contains(description);
+ });
+ });
+
+ it('renders expander with predicateOverride prop', () => {
+ cy.get('#expanderWithPredicateOverride').within(() => {
+ // predicateOverride filters out all items but one.
+ cy.get(`[data-testid=expander-item]`).should('have.length', 1);
+ });
+ });
+});
diff --git a/packages/test-generator/integration-test-templates/src/App.tsx b/packages/test-generator/integration-test-templates/src/App.tsx
index a394b2bfb..ee9d503b3 100644
--- a/packages/test-generator/integration-test-templates/src/App.tsx
+++ b/packages/test-generator/integration-test-templates/src/App.tsx
@@ -19,6 +19,7 @@ import ComponentTests from './ComponentTests';
import GenerateTests from './GenerateTests';
import PrimitivesTests from './PrimitivesTests';
import ComplexTests from './ComplexTests';
+import ViewTests from './ViewTests';
import SnippetTests from './SnippetTests'; // eslint-disable-line import/extensions
import WorkflowTests from './WorkflowTests';
import TwoWayBindingTests from './TwoWayBindingTests';
@@ -55,6 +56,9 @@ const HomePage = () => {
Workflow Tests
+
+ View Tests
+
Two Way Binding Tests
@@ -79,6 +83,7 @@ export default function App() {
} />
} />
} />
+ } />
} />
} />
} />
diff --git a/packages/test-generator/integration-test-templates/src/ViewTests.tsx b/packages/test-generator/integration-test-templates/src/ViewTests.tsx
new file mode 100644
index 000000000..117ef414d
--- /dev/null
+++ b/packages/test-generator/integration-test-templates/src/ViewTests.tsx
@@ -0,0 +1,71 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License").
+ You may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+import '@aws-amplify/ui-react/styles.css';
+import { AmplifyProvider, View, Heading } from '@aws-amplify/ui-react';
+import { createDataStorePredicate } from '@aws-amplify/ui-react/internal';
+import { useState, useRef, useEffect } from 'react';
+import { ListingExpanderWithComponentSlot, MyTheme } from './ui-components'; // eslint-disable-line import/extensions
+import { initializeListingTestData } from './mock-utils';
+
+export default function FormTests() {
+ const [isInitialized, setInitialized] = useState(false);
+ const initializeStarted = useRef(false);
+
+ useEffect(() => {
+ const initializeTestUserData = async () => {
+ if (initializeStarted.current) {
+ return;
+ }
+ // DataStore.clear() doesn't appear to reliably work in this scenario.
+ const ddbRequest = indexedDB.deleteDatabase('amplify-datastore');
+ await new Promise((res, rej) => {
+ ddbRequest.onsuccess = () => {
+ res(true);
+ };
+ ddbRequest.onerror = () => {
+ rej(ddbRequest.error);
+ };
+ });
+ await Promise.all([initializeListingTestData()]);
+ setInitialized(true);
+ };
+
+ initializeTestUserData();
+ initializeStarted.current = true;
+ }, []);
+
+ if (!isInitialized) {
+ return null;
+ }
+ return (
+
+
+ Expander with Component Slot
+
+
+
+ Expander with Component Slot
+
+
+
+ );
+}
diff --git a/packages/test-generator/lib/generators/BrowserTestGenerator.ts b/packages/test-generator/lib/generators/BrowserTestGenerator.ts
index b5d83a67f..a5ef7fb45 100644
--- a/packages/test-generator/lib/generators/BrowserTestGenerator.ts
+++ b/packages/test-generator/lib/generators/BrowserTestGenerator.ts
@@ -21,6 +21,7 @@ import {
StudioComponent,
StudioForm,
StudioTheme,
+ StudioView,
} from '@aws-amplify/codegen-ui';
import {
AmplifyRenderer,
@@ -29,6 +30,7 @@ import {
ReactUtilsStudioTemplateRenderer,
AmplifyFormRenderer,
UtilTemplateType,
+ AmplifyViewRenderer,
} from '@aws-amplify/codegen-ui-react';
import schema from '../models/schema';
import { TestGenerator } from './TestGenerator';
@@ -42,6 +44,8 @@ export class BrowserTestGenerator extends TestGenerator {
return { formMetadata: {} as FormMetadata };
} // no-op
+ writeViewToDisk() {} // no-op
+
writeIndexFileToDisk() {} // no-op
writeUtilsFileToDisk() {} // no-op
@@ -64,6 +68,10 @@ export class BrowserTestGenerator extends TestGenerator {
return new AmplifyFormRenderer(form, getGenericFromDataStore(schema), this.renderConfig).renderComponentOnly();
}
+ renderView(view: StudioView) {
+ return new AmplifyViewRenderer(view, getGenericFromDataStore(schema), this.renderConfig).renderComponentOnly();
+ }
+
renderTheme(theme: StudioTheme) {
return new ReactThemeStudioTemplateRenderer(theme, this.renderConfig).renderComponent();
}
diff --git a/packages/test-generator/lib/generators/NodeTestGenerator.ts b/packages/test-generator/lib/generators/NodeTestGenerator.ts
index 1cdca03d5..390872da0 100644
--- a/packages/test-generator/lib/generators/NodeTestGenerator.ts
+++ b/packages/test-generator/lib/generators/NodeTestGenerator.ts
@@ -25,6 +25,7 @@ import {
StudioForm,
getGenericFromDataStore,
StudioSchema,
+ StudioView,
} from '@aws-amplify/codegen-ui';
import {
AmplifyRenderer,
@@ -33,6 +34,7 @@ import {
ReactUtilsStudioTemplateRenderer,
AmplifyFormRenderer,
UtilTemplateType,
+ AmplifyViewRenderer,
} from '@aws-amplify/codegen-ui-react';
import schema from '../models/schema';
import { TestGenerator, TestGeneratorParams } from './TestGenerator';
@@ -44,6 +46,8 @@ export class NodeTestGenerator extends TestGenerator {
private readonly formRendererFactory: any;
+ private readonly viewRendererFactory: any;
+
private readonly indexRendererFactory: any;
private readonly utilsRendererFactory: any;
@@ -52,6 +56,8 @@ export class NodeTestGenerator extends TestGenerator {
private readonly formRendererManager: any;
+ private readonly viewRendererManager: any;
+
private readonly themeRendererManager: any;
private readonly indexRendererManager: any;
@@ -70,6 +76,9 @@ export class NodeTestGenerator extends TestGenerator {
this.formRendererFactory = new StudioTemplateRendererFactory(
(form: StudioForm) => new AmplifyFormRenderer(form, getGenericFromDataStore(schema), this.renderConfig),
);
+ this.viewRendererFactory = new StudioTemplateRendererFactory(
+ (view: StudioView) => new AmplifyViewRenderer(view, getGenericFromDataStore(schema), this.renderConfig),
+ );
this.indexRendererFactory = new StudioTemplateRendererFactory(
(schemas: StudioSchema[]) => new ReactIndexStudioTemplateRenderer(schemas, this.renderConfig),
);
@@ -78,6 +87,7 @@ export class NodeTestGenerator extends TestGenerator {
);
this.componentRendererManager = new StudioTemplateRendererManager(this.componentRendererFactory, this.outputConfig);
this.formRendererManager = new StudioTemplateRendererManager(this.formRendererFactory, this.outputConfig);
+ this.viewRendererManager = new StudioTemplateRendererManager(this.viewRendererFactory, this.outputConfig);
this.themeRendererManager = new StudioTemplateRendererManager(this.themeRendererFactory, this.outputConfig);
this.indexRendererManager = new StudioTemplateRendererManager(this.indexRendererFactory, this.outputConfig);
this.utilsRendererManager = new StudioTemplateRendererManager(this.utilsRendererFactory, this.outputConfig);
@@ -96,11 +106,20 @@ export class NodeTestGenerator extends TestGenerator {
return this.formRendererManager.renderSchemaToTemplate(form);
}
+ writeViewToDisk(view: StudioView) {
+ return this.viewRendererManager.renderSchemaToTemplate(view);
+ }
+
renderForm(form: StudioForm) {
const buildRenderer = this.formRendererFactory.buildRenderer(form);
return buildRenderer.renderComponentOnly();
}
+ renderView(view: StudioView) {
+ const buildRenderer = this.viewRendererFactory.buildRenderer(view);
+ return buildRenderer.renderComponentOnly();
+ }
+
writeSnippetToDisk(components: StudioComponent[]) {
const { importsText, compText } = this.renderSnippet(components);
fs.writeFileSync(path.join(this.outputConfig.outputPathDir, '..', 'SnippetTests.jsx'), importsText + compText);
diff --git a/packages/test-generator/lib/generators/TestGenerator.ts b/packages/test-generator/lib/generators/TestGenerator.ts
index 8d6f08587..f568c1ce9 100644
--- a/packages/test-generator/lib/generators/TestGenerator.ts
+++ b/packages/test-generator/lib/generators/TestGenerator.ts
@@ -13,7 +13,7 @@
See the License for the specific language governing permissions and
limitations under the License.
*/
-import { FormMetadata, StudioComponent, StudioForm, StudioTheme } from '@aws-amplify/codegen-ui';
+import { FormMetadata, StudioComponent, StudioForm, StudioTheme, StudioView } from '@aws-amplify/codegen-ui';
import {
ModuleKind,
ScriptTarget,
@@ -26,6 +26,7 @@ import log from 'loglevel';
import * as ComponentSchemas from '../components';
import * as ThemeSchemas from '../themes';
import * as FormSchemas from '../forms';
+import * as ViewSchemas from '../views';
const DEFAULT_RENDER_CONFIG = {
module: ModuleKind.CommonJS,
@@ -42,7 +43,7 @@ log.setLevel('info');
export type TestCase = {
name: string;
schema: any;
- testType: 'Component' | 'Theme' | 'Form' | 'Snippet';
+ testType: 'Component' | 'Theme' | 'Form' | 'Snippet' | 'View';
};
export type TestGeneratorParams = {
@@ -143,6 +144,30 @@ export abstract class TestGenerator {
}
};
+ const generateView = (testCase: TestCase) => {
+ const { name, schema } = testCase;
+ try {
+ if (this.params.writeToDisk) {
+ this.writeViewToDisk(schema as StudioView);
+ }
+
+ if (this.params.writeToLogger) {
+ const { importsText, compText } = this.renderView(schema as StudioView);
+ log.info(`# ${name}`);
+ log.info('## View Only Output');
+ log.info('### viewImports');
+ log.info(this.decorateTypescriptWithMarkdown(importsText));
+ log.info('### viewText');
+ log.info(this.decorateTypescriptWithMarkdown(compText));
+ }
+ } catch (err) {
+ if (this.params.immediatelyThrowGenerateErrors) {
+ throw err;
+ }
+ renderErrors[name] = err;
+ }
+ };
+
const generateIndexFile = (indexFileTestCases: TestCase[]) => {
const schemas = indexFileTestCases.map((testCase) => testCase.schema);
try {
@@ -217,6 +242,9 @@ export abstract class TestGenerator {
case 'Snippet':
generateSnippet([testCase]);
break;
+ case 'View':
+ generateView(testCase);
+ break;
default:
throw new Error('Expected either a `Component`, `Theme`, `Form` test case type');
}
@@ -251,12 +279,16 @@ export abstract class TestGenerator {
abstract writeFormToDisk(form: StudioForm): { formMetadata: FormMetadata };
+ abstract writeViewToDisk(view: StudioView): void;
+
abstract renderComponent(component: StudioComponent): { compText: string; importsText: string };
abstract renderTheme(theme: StudioTheme): { componentText: string };
abstract renderForm(form: StudioForm): { compText: string; importsText: string };
+ abstract renderView(view: StudioView): { compText: string; importsText: string };
+
abstract writeIndexFileToDisk(schemas: (StudioComponent | StudioForm | StudioTheme)[]): void;
abstract renderIndexFile(schemas: (StudioComponent | StudioForm | StudioTheme)[]): { componentText: string };
@@ -281,6 +313,9 @@ export abstract class TestGenerator {
...Object.entries(FormSchemas).map(([name, schema]) => {
return { name, schema, testType: 'Form' } as TestCase;
}),
+ ...Object.entries(ViewSchemas).map(([name, schema]) => {
+ return { name, schema, testType: 'View' } as TestCase;
+ }),
].filter((testCase) => !disabledSchemaSet.has(testCase.name));
}
}
diff --git a/packages/test-generator/lib/index.ts b/packages/test-generator/lib/index.ts
index 28c71e4b9..a1bdc4c62 100644
--- a/packages/test-generator/lib/index.ts
+++ b/packages/test-generator/lib/index.ts
@@ -17,3 +17,4 @@ export * from './components';
export * from './generators';
export * from './themes';
export * from './forms';
+export * from './views';
diff --git a/packages/test-generator/lib/views/index.ts b/packages/test-generator/lib/views/index.ts
new file mode 100644
index 000000000..24cd2264b
--- /dev/null
+++ b/packages/test-generator/lib/views/index.ts
@@ -0,0 +1,16 @@
+/*
+ Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+ Licensed under the Apache License, Version 2.0 (the "License").
+ You may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+ */
+export { default as ListingExpanderWithComponentSlot } from './listing-expander-with-component-slot.json';
diff --git a/packages/test-generator/lib/views/listing-expander-with-component-slot.json b/packages/test-generator/lib/views/listing-expander-with-component-slot.json
new file mode 100644
index 000000000..cedd9ca53
--- /dev/null
+++ b/packages/test-generator/lib/views/listing-expander-with-component-slot.json
@@ -0,0 +1,47 @@
+{
+ "appId": "d1234",
+ "dataSource": {
+ "model": "Listing",
+ "sort": [
+ {
+ "field": "title",
+ "direction": "DESC"
+ }
+ ],
+ "type": "DataStore"
+ },
+ "environmentName": "staging",
+ "id": "v-123456",
+ "name": "ListingExpanderWithComponentSlot",
+ "schemaVersion": "1.0",
+ "style": {},
+ "viewConfiguration": {
+ "type": "Collection",
+ "collection": {
+ "collectionType": "expander",
+ "title": {
+ "type": "Amplify.Binding",
+ "content": {
+ "bindingProperty": {
+ "property": "item",
+ "field": "title"
+ }
+ }
+ },
+ "body": {
+ "type": "Amplify.ComponentSlot",
+ "content": {
+ "componentSlot": {
+ "componentName": "Button",
+ "bindingProperties": {
+ "children": {
+ "property": "item",
+ "field": "description"
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file