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 e8d964779..8049c1577 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,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`amplify view renderer tests should generate a non-datastore table element 1`] = ` +exports[`amplify table renderer tests should generate a non-datastore table element 1`] = ` " {!disableHeaders && ( @@ -32,7 +32,7 @@ exports[`amplify view renderer tests should generate a non-datastore table eleme " `; -exports[`amplify view renderer tests should generate a table element 1`] = ` +exports[`amplify table renderer tests should generate a table element 1`] = ` "
{!disableHeaders && ( @@ -79,3 +79,299 @@ exports[`amplify view renderer tests should generate a table element 1`] = `
; " `; + +exports[`amplify view renderer tests should call util file if rendered 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { formatter } from \\"./utils\\"; +import { + createDataStorePredicate, + useDataStoreBinding, +} from \\"@aws-amplify/ui-react/internal\\"; +import { SortDirection } from \\"@aws-amplify/datastore\\"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from \\"@aws-amplify/ui-react\\"; +export default function MyPostTable(props) { + const { + items: itemsProps, + predicateOverride, + formatOverride, + highlightOnHover, + onRowClick, + disableHeaders, + ...rest + } = props; + const postFilter = { + and: [ + { field: \\"username\\", operand: \\"Guy\\", operator: \\"notContains\\" }, + { field: \\"createdAt\\", operand: \\"25\\", operator: \\"contains\\" }, + ], + }; + const postPredicate = createDataStorePredicate(postFilter); + const postPagination = { sort: (s) => s.username(SortDirection.ASCENDING) }; + const MyPostTableDataStore = useDataStoreBinding({ + type: \\"collection\\", + model: Post, + criteria: predicateOverride || postPredicate, + pagination: postPagination, + }).items; + const items = itemsProp !== undefined ? itemsProp : MyPostTableDataStore; + return ( + + {!disableHeaders && ( + + + id + caption + username + post_url + profile_url + status + createdAt + updatedAt + + + )} + + {items.map((item, index) => ( + onRowClick(item, index) : null}> + {format?.id ? format.id(item?.id) : item?.id} + + {format?.caption ? format.caption(item?.caption) : item?.caption} + + + {format?.username + ? format.username(item?.username) + : item?.username} + + + {format?.post_url + ? format.post_url(item?.post_url) + : item?.post_url} + + + {format?.profile_url + ? format.profile_url(item?.profile_url) + : item?.profile_url} + + + {format?.status ? format.status(item?.status) : item?.status} + + + {format?.createdAt + ? format.createdAt(item?.createdAt) + : formatter(item?.createdAt, { + type: \\"DateTimeFormat\\", + format: { + dateTimeFormat: { + dateFormat: \\"MM/DD/YYYY\\", + timeFormat: \\"locale\\", + }, + }, + })} + + + {format?.updatedAt + ? format.updatedAt(item?.updatedAt) + : item?.updatedAt} + + + ))} + +
+ ); +} +" +`; + +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\\"; +export declare type MyPostTableProps = React.PropsWithChildren<{ + overrides?: EscapeHatchProps | undefined | null; +}>; +export default function MyPostTable(props: MyPostTableProps): React.ReactElement; +" +`; + +exports[`amplify view renderer tests should render view with custom datastore 1`] = ` +"/* eslint-disable */ +import * as React from \\"react\\"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from \\"@aws-amplify/ui-react\\"; +export default function CustomTable(props) { + const { + items, + formatOverride, + highlightOnHover, + onRowClick, + disableHeaders, + ...rest + } = props; + return ( + + {!disableHeaders && ( + + + name + age + address + birthday + + + )} + + {items.map((item, index) => ( + onRowClick(item, index) : null}> + + {format?.name ? format.name(item?.name) : item?.name} + + + {format?.age ? format.age(item?.age) : item?.age} + + + {format?.address ? format.address(item?.address) : item?.address} + + + {format?.birthday + ? format.birthday(item?.birthday) + : item?.birthday} + + + ))} + +
+ ); +} +" +`; + +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\\"; +export declare type CustomTableProps = React.PropsWithChildren<{ + overrides?: EscapeHatchProps | undefined | null; +}>; +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 { + createDataStorePredicate, + useDataStoreBinding, +} from \\"@aws-amplify/ui-react/internal\\"; +import { SortDirection } from \\"@aws-amplify/datastore\\"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableRow, +} from \\"@aws-amplify/ui-react\\"; +export default function MyPostTable(props) { + const { + items: itemsProps, + predicateOverride, + formatOverride, + highlightOnHover, + onRowClick, + disableHeaders, + ...rest + } = props; + const postFilter = { + and: [ + { field: \\"username\\", operand: \\"username0\\", operator: \\"notContains\\" }, + { field: \\"createdAt\\", operand: \\"2022\\", operator: \\"contains\\" }, + ], + }; + const postPredicate = createDataStorePredicate(postFilter); + const postPagination = { sort: (s) => s.username(SortDirection.ASCENDING) }; + const MyPostTableDataStore = useDataStoreBinding({ + type: \\"collection\\", + model: Post, + criteria: predicateOverride || postPredicate, + pagination: postPagination, + }).items; + const items = itemsProp !== undefined ? itemsProp : MyPostTableDataStore; + return ( + + {!disableHeaders && ( + + + id + caption + username + post_url + profile_url + status + createdAt + updatedAt + + + )} + + {items.map((item, index) => ( + onRowClick(item, index) : null}> + {format?.id ? format.id(item?.id) : item?.id} + + {format?.caption ? format.caption(item?.caption) : item?.caption} + + + {format?.username + ? format.username(item?.username) + : item?.username} + + + {format?.post_url + ? format.post_url(item?.post_url) + : item?.post_url} + + + {format?.profile_url + ? format.profile_url(item?.profile_url) + : item?.profile_url} + + + {format?.status ? format.status(item?.status) : item?.status} + + + {format?.createdAt + ? format.createdAt(item?.createdAt) + : item?.createdAt} + + + {format?.updatedAt + ? format.updatedAt(item?.updatedAt) + : item?.updatedAt} + + + ))} + +
+ ); +} +" +`; + +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\\"; +export declare type MyPostTableProps = React.PropsWithChildren<{ + overrides?: EscapeHatchProps | 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 77ce0128a..fb68856d9 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 @@ -70,6 +70,24 @@ export const generateWithAmplifyFormRenderer = ( return renderer.renderComponent(); }; +export const renderWithAmplifyViewRenderer = ( + viewJsonFile: string, + dataSchemaJsonFile: string | undefined, + renderConfig: ReactRenderConfig = defaultCLIRenderConfig, +): { componentText: string; declaration?: string } => { + let dataSchema: GenericDataSchema | undefined; + if (dataSchemaJsonFile) { + const dataStoreSchema = loadSchemaFromJSONFile(dataSchemaJsonFile); + dataSchema = getGenericFromDataStore(dataStoreSchema); + } + const rendererFactory = new StudioTemplateRendererFactory( + (view: StudioView) => new AmplifyViewRenderer(view, dataSchema, renderConfig), + ); + + const renderer = rendererFactory.buildRenderer(loadSchemaFromJSONFile(viewJsonFile)); + return renderer.renderComponent(); +}; + export const renderTableJsxElement = ( tableFilePath: string, dataSchemaFilePath: string | undefined, 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 7808eea87..ac36b8c38 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,9 +13,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { renderTableJsxElement } from './__utils__'; +import { renderTableJsxElement, renderWithAmplifyViewRenderer } from './__utils__'; -describe('amplify view renderer tests', () => { +describe('amplify table renderer tests', () => { test('should generate a table element', () => { const tableElement = renderTableJsxElement('views/table-from-datastore', 'datastore/person', 'test-table.ts'); expect(tableElement).toMatchSnapshot(); @@ -26,3 +26,31 @@ describe('amplify view renderer tests', () => { expect(tableElement).toMatchSnapshot(); }); }); + +describe('amplify view renderer tests', () => { + test('should render view with passed in predicate and sort', () => { + const { componentText, declaration } = renderWithAmplifyViewRenderer( + 'views/post-table-datastore', + 'datastore/post-ds', + ); + expect(componentText).toContain('useDataStoreBinding'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + test('should render view with custom datastore', () => { + const { componentText, declaration } = renderWithAmplifyViewRenderer('views/table-from-custom-json', undefined); + expect(componentText).not.toContain('useDataStoreBinding'); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); + + test('should call util file if rendered', () => { + const { componentText, declaration } = renderWithAmplifyViewRenderer( + 'views/post-table-custom-format', + 'datastore/post-ds', + ); + expect(componentText.replace(/\\/g, '')).toContain(`import { formatter } from "./utils"`); + expect(componentText).toMatchSnapshot(); + expect(declaration).toMatchSnapshot(); + }); +}); 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 fe6f3dca9..eed37e3cf 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 @@ -25,7 +25,7 @@ export class AmplifyViewRenderer extends ReactViewTemplateRenderer { case Primitive.Table: return new ReactTableRenderer( this.viewComponent, - this.viewDefinition!, + this.viewDefinition, this.viewMetadata, this.importCollection, ).renderElement(); diff --git a/packages/codegen-ui-react/lib/imports/import-mapping.ts b/packages/codegen-ui-react/lib/imports/import-mapping.ts index 712bf2e32..807a085e3 100644 --- a/packages/codegen-ui-react/lib/imports/import-mapping.ts +++ b/packages/codegen-ui-react/lib/imports/import-mapping.ts @@ -46,6 +46,7 @@ export enum ImportValue { USE_EFFECT = 'useEffect', VALIDATE_FIELD = 'validateField', VALIDATE_FIELD_CODEGEN = 'validateField', + FORMATTER = 'formatter', } export const ImportMapping: Record = { @@ -68,6 +69,7 @@ export const ImportMapping: Record = { [ImportValue.USE_AUTH_SIGN_OUT_ACTION]: ImportSource.UI_REACT_INTERNAL, [ImportValue.USE_STATE_MUTATION_ACTION]: ImportSource.UI_REACT_INTERNAL, [ImportValue.USE_EFFECT]: ImportSource.REACT, + [ImportValue.FORMATTER]: ImportSource.UTILS, [ImportValue.VALIDATE_FIELD]: ImportSource.UTILS, [ImportValue.VALIDATE_FIELD_CODEGEN]: ImportSource.CODEGEN_UI_REACT, }; diff --git a/packages/codegen-ui-react/lib/index.ts b/packages/codegen-ui-react/lib/index.ts index 3a38bdcbb..c21e41a25 100644 --- a/packages/codegen-ui-react/lib/index.ts +++ b/packages/codegen-ui-react/lib/index.ts @@ -23,6 +23,7 @@ export * from './react-render-config'; export * from './react-output-manager'; export * from './amplify-ui-renderers/amplify-renderer'; export * from './amplify-ui-renderers/amplify-form-renderer'; +export * from './amplify-ui-renderers/amplify-view-renderer'; export * from './primitive'; export * from './react-index-studio-template-renderer'; export * from './react-utils-studio-template-renderer'; diff --git a/packages/codegen-ui-react/lib/react-studio-template-renderer-helper.ts b/packages/codegen-ui-react/lib/react-studio-template-renderer-helper.ts index 986d88900..1f688cf9a 100644 --- a/packages/codegen-ui-react/lib/react-studio-template-renderer-helper.ts +++ b/packages/codegen-ui-react/lib/react-studio-template-renderer-helper.ts @@ -21,6 +21,7 @@ import { InternalError, InvalidInputError, StudioComponentSlotBinding, + StudioComponentSort, } from '@aws-amplify/codegen-ui'; import ts, { createPrinter, @@ -38,6 +39,9 @@ import ts, { BindingName, Expression, PropertyAssignment, + ArrowFunction, + CallExpression, + Identifier, } from 'typescript'; import { createDefaultMapFromNodeModules, createSystem, createVirtualCompilerHost } from '@typescript/vfs'; import path from 'path'; @@ -145,6 +149,50 @@ export function buildPrinter(fileName: string, renderConfig: ReactRenderConfig) return { printer, file }; } +/** + * (s: SortPredicate) => s.firstName('ASCENDING').lastName('DESCENDING') + */ +export const buildSortFunction = (model: string, sort: StudioComponentSort[]): ArrowFunction => { + const ascendingSortDirection = factory.createPropertyAccessExpression( + factory.createIdentifier('SortDirection'), + factory.createIdentifier('ASCENDING'), + ); + const descendingSortDirection = factory.createPropertyAccessExpression( + factory.createIdentifier('SortDirection'), + factory.createIdentifier('DESCENDING'), + ); + + let expr: Identifier | CallExpression = factory.createIdentifier('s'); + sort.forEach((sortPredicate) => { + expr = factory.createCallExpression( + factory.createPropertyAccessExpression(expr, factory.createIdentifier(sortPredicate.field)), + undefined, + [sortPredicate.direction === 'ASC' ? ascendingSortDirection : descendingSortDirection], + ); + }); + + return factory.createArrowFunction( + undefined, + undefined, + [ + factory.createParameterDeclaration( + undefined, + undefined, + undefined, + factory.createIdentifier('s'), + undefined, + factory.createTypeReferenceNode(factory.createIdentifier('SortPredicate'), [ + factory.createTypeReferenceNode(factory.createIdentifier(model), undefined), + ]), + undefined, + ), + ], + undefined, + factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), + expr, + ); +}; + export function getDeclarationFilename(filename: string): string { return `${path.basename(filename, filename.includes('.tsx') ? '.tsx' : '.jsx')}.d.ts`; } diff --git a/packages/codegen-ui-react/lib/react-studio-template-renderer.ts b/packages/codegen-ui-react/lib/react-studio-template-renderer.ts index 454527b01..5eca88ddd 100644 --- a/packages/codegen-ui-react/lib/react-studio-template-renderer.ts +++ b/packages/codegen-ui-react/lib/react-studio-template-renderer.ts @@ -52,8 +52,6 @@ import ts, { Modifier, ObjectLiteralExpression, CallExpression, - Identifier, - ArrowFunction, LiteralExpression, BooleanLiteral, addSyntheticLeadingComment, @@ -77,6 +75,7 @@ import { buildPropAssignmentWithFilter, buildCollectionWithItemMap, createHookStatement, + buildSortFunction, } from './react-studio-template-renderer-helper'; import { Primitive, isPrimitive, PrimitiveTypeParameter, PrimitiveChildrenPropMapping } from './primitive'; import { RequiredKeys } from './utils/type-utils'; @@ -1056,12 +1055,7 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer factory.createObjectLiteralExpression( ([] as ts.PropertyAssignment[]).concat( sort - ? [ - factory.createPropertyAssignment( - factory.createIdentifier('sort'), - this.buildSortFunction(model, sort), - ), - ] + ? [factory.createPropertyAssignment(factory.createIdentifier('sort'), buildSortFunction(model, sort))] : [], ), ), @@ -1072,50 +1066,6 @@ export abstract class ReactStudioTemplateRenderer extends StudioTemplateRenderer ); } - /** - * (s: SortPredicate) => s.firstName('ASCENDING').lastName('DESCENDING') - */ - private buildSortFunction(model: string, sort: StudioComponentSort[]): ArrowFunction { - const ascendingSortDirection = factory.createPropertyAccessExpression( - factory.createIdentifier('SortDirection'), - factory.createIdentifier('ASCENDING'), - ); - const descendingSortDirection = factory.createPropertyAccessExpression( - factory.createIdentifier('SortDirection'), - factory.createIdentifier('DESCENDING'), - ); - - let expr: Identifier | CallExpression = factory.createIdentifier('s'); - sort.forEach((sortPredicate) => { - expr = factory.createCallExpression( - factory.createPropertyAccessExpression(expr, factory.createIdentifier(sortPredicate.field)), - undefined, - [sortPredicate.direction === 'ASC' ? ascendingSortDirection : descendingSortDirection], - ); - }); - - return factory.createArrowFunction( - undefined, - undefined, - [ - factory.createParameterDeclaration( - undefined, - undefined, - undefined, - factory.createIdentifier('s'), - undefined, - factory.createTypeReferenceNode(factory.createIdentifier('SortPredicate'), [ - factory.createTypeReferenceNode(factory.createIdentifier(model), undefined), - ]), - undefined, - ), - ], - undefined, - factory.createToken(ts.SyntaxKind.EqualsGreaterThanToken), - expr, - ); - } - private buildCollectionBindingCall( model: string, modelVariableName: string, 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 0e7f4d9c1..d306180c0 100644 --- a/packages/codegen-ui-react/lib/react-table-renderer-helper.ts +++ b/packages/codegen-ui-react/lib/react-table-renderer-helper.ts @@ -13,8 +13,76 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { StringFormat, ViewValueFormatting } from '@aws-amplify/codegen-ui/lib/types'; -import { factory, ObjectLiteralExpression } from 'typescript'; +import { StringFormat, TableConfiguration, ViewValueFormatting } from '@aws-amplify/codegen-ui/lib/types'; +import { CallExpression, factory, ObjectLiteralExpression, SyntaxKind } from 'typescript'; + +export const getFilterName = (model: string) => `${model.toLowerCase()}Filter`; +export const getPredicateName = (model: string) => `${model.toLowerCase()}Predicate`; +export const getPaginationName = (model: string) => `${model.toLowerCase()}Pagination`; + +/* +checks table to see if there is a formatter for stringFormat +*/ +export const needsFormatter = (table: TableConfiguration): boolean => { + if (table.columns) { + return Object.values(table.columns).some((column) => column.valueFormatting?.stringFormat !== undefined); + } + return false; +}; + +/* + const dataStoreItems = useDataStoreBinding({ + type: 'collection', + model: 'Post', + criteria: predicateOverrides ?? predicateApiSettings, + sort: sortApiSettings, + }) + */ +export const buildDataStoreCollectionCall = ( + model: string, + criteriaName?: string, + paginationName?: string, +): CallExpression => { + const objectProperties = [ + factory.createPropertyAssignment(factory.createIdentifier('type'), factory.createStringLiteral('collection')), + factory.createPropertyAssignment(factory.createIdentifier('model'), factory.createIdentifier(model)), + ] + .concat( + criteriaName + ? [ + // criteria: predicateOverride || {criteriaName} + factory.createPropertyAssignment( + factory.createIdentifier('criteria'), + factory.createBinaryExpression( + factory.createIdentifier('predicateOverride'), + factory.createToken(SyntaxKind.BarBarToken), + factory.createIdentifier(criteriaName), + ), + ), + ] + : [ + // criteria: predicateOverride + factory.createPropertyAssignment( + factory.createIdentifier('criteria'), + factory.createIdentifier('predicateOverride'), + ), + ], + ) + .concat( + paginationName + ? [ + factory.createPropertyAssignment( + factory.createIdentifier('pagination'), + factory.createIdentifier(paginationName), + ), + ] + : [], + ); + + return factory.createCallExpression(factory.createIdentifier('useDataStoreBinding'), undefined, [ + factory.createObjectLiteralExpression(objectProperties, true), + ]); +}; /* Helper to codegen objects diff --git a/packages/codegen-ui-react/lib/react-table-renderer.ts b/packages/codegen-ui-react/lib/react-table-renderer.ts index 2a1461a51..1c470fae4 100644 --- a/packages/codegen-ui-react/lib/react-table-renderer.ts +++ b/packages/codegen-ui-react/lib/react-table-renderer.ts @@ -50,11 +50,11 @@ export class ReactTableRenderer { this.viewComponent = view; this.viewDefinition = definition; this.viewMetadata = metadata; - this.viewMetadata.tableFieldFormatting = {}; + this.viewMetadata.fieldFormatting = {}; this.viewDefinition.columns.forEach((column) => { if (column.valueFormatting) { - this.viewMetadata.tableFieldFormatting![column.header] = { ...column.valueFormatting }; + this.viewMetadata.fieldFormatting[column.header] = { ...column.valueFormatting }; } }); @@ -160,7 +160,7 @@ export class ReactTableRenderer { } generateFormatLiteralExpression(field: string): ObjectLiteralExpression | Identifier { - const formatting = this.viewMetadata.tableFieldFormatting; + const formatting = this.viewMetadata.fieldFormatting; if (formatting?.[field]) { return objectToExpression(formatting[field].stringFormat); @@ -186,7 +186,7 @@ export class ReactTableRenderer { } */ createFormatArg(field: string) { - const format = this.viewMetadata.tableFieldFormatting?.[field]; + const format = this.viewMetadata.fieldFormatting?.[field]; const type: StringFormat['type'] | undefined = stringFormatToType(format); @@ -203,7 +203,7 @@ export class ReactTableRenderer { } createFormatCallOrPropAccess(field: string) { - const format = this.viewMetadata.tableFieldFormatting?.[field]; + const format = this.viewMetadata.fieldFormatting?.[field]; return format ? factory.createCallExpression(factory.createIdentifier('formatter'), undefined, [ factory.createPropertyAccessChain( diff --git a/packages/codegen-ui-react/lib/react-utils-studio-template-renderer.ts b/packages/codegen-ui-react/lib/react-utils-studio-template-renderer.ts index 2a4a7f47b..76bec98cf 100644 --- a/packages/codegen-ui-react/lib/react-utils-studio-template-renderer.ts +++ b/packages/codegen-ui-react/lib/react-utils-studio-template-renderer.ts @@ -24,11 +24,11 @@ import { transpile, buildPrinter, defaultRenderConfig } from './react-studio-tem import { generateValidationFunction } from './utils/forms/validation'; import { generateFormatUtil } from './utils/string-formatter'; -export type Util = string; +export type UtilTemplateType = 'validation' | 'formatter'; export class ReactUtilsStudioTemplateRenderer extends StudioTemplateRenderer< string, - string[], + UtilTemplateType[], ReactOutputManager, { componentText: string; @@ -44,9 +44,9 @@ export class ReactUtilsStudioTemplateRenderer extends StudioTemplateRenderer< /* * list of util functions to generate */ - utils: Util[]; + utils: UtilTemplateType[]; - constructor(utils: Util[], renderConfig: ReactRenderConfig) { + constructor(utils: UtilTemplateType[], renderConfig: ReactRenderConfig) { super(utils, new ReactOutputManager(), renderConfig); this.utils = utils; this.renderConfig = { 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 8e3245bba..7b1a482f0 100644 --- a/packages/codegen-ui-react/lib/views/react-view-renderer.ts +++ b/packages/codegen-ui-react/lib/views/react-view-renderer.ts @@ -24,6 +24,7 @@ import { ViewMetadata, handleCodegenErrors, validateViewSchema, + StudioComponentPredicate, } from '@aws-amplify/codegen-ui'; import { addSyntheticLeadingComment, @@ -36,6 +37,7 @@ import { JsxSelfClosingElement, Modifier, NodeFlags, + ObjectLiteralExpression, ScriptKind, Statement, SyntaxKind, @@ -45,6 +47,7 @@ import { EOL } from 'os'; import { buildBaseCollectionVariableStatement, buildPrinter, + buildSortFunction, defaultRenderConfig, getDeclarationFilename, transpile, @@ -55,6 +58,13 @@ import { getComponentPropName } from '../react-component-render-helper'; import { ReactOutputManager } from '../react-output-manager'; import { ReactRenderConfig, scriptKindToFileExtension } from '../react-render-config'; import { RequiredKeys } from '../utils/type-utils'; +import { + buildDataStoreCollectionCall, + getFilterName, + getPaginationName, + getPredicateName, + needsFormatter, +} from '../react-table-renderer-helper'; export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer< string, @@ -69,7 +79,7 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer< protected renderConfig: RequiredKeys; - protected viewDefinition: TableDefinition | undefined; + protected viewDefinition: TableDefinition; protected viewComponent: StudioView; @@ -93,14 +103,20 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer< this.viewDefinition = generateTableDefinition(component, dataSchema); break; default: - this.viewDefinition = undefined; + throw new Error(`Type: ${component.viewConfiguration.type} 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, + fieldFormatting: {}, }; } @@ -235,11 +251,28 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer< buildVariableStatements() { const statements: Statement[] = []; const elements: BindingElement[] = []; + const { type, model, predicate, sort } = this.viewComponent.dataSource; + const isDataStoreEnabled = type === 'DataStore' && model; + if (isDataStoreEnabled) { + this.importCollection.addImport(ImportSource.LOCAL_MODELS, this.component.dataSource.type); + this.importCollection.addMappedImport(ImportValue.USE_DATA_STORE_BINDING); + elements.push( + factory.createBindingElement( + undefined, + factory.createIdentifier('items'), + factory.createIdentifier('itemsProps'), + undefined, + ), + factory.createBindingElement(undefined, undefined, factory.createIdentifier('predicateOverride'), undefined), + ); + } else { + elements.push(factory.createBindingElement(undefined, undefined, factory.createIdentifier('items'), undefined)); + } + + // add base Props // props const props = [ - factory.createBindingElement(undefined, undefined, factory.createIdentifier('items'), undefined), - factory.createBindingElement(undefined, undefined, factory.createIdentifier('predicateOverride'), undefined), factory.createBindingElement(undefined, undefined, factory.createIdentifier('formatOverride'), undefined), factory.createBindingElement(undefined, undefined, factory.createIdentifier('highlightOnHover'), undefined), factory.createBindingElement(undefined, undefined, factory.createIdentifier('onRowClick'), undefined), @@ -275,81 +308,138 @@ export abstract class ReactViewTemplateRenderer extends StudioTemplateRenderer< ), ); - // add model import for datastore type - if (this.component.dataSource.type === 'DataStore') { - this.importCollection.addImport(ImportSource.LOCAL_MODELS, this.component.dataSource.type); - } - - /* - if datastore enabled - const myViewDataStore = useDataStoreBinding({ - model: Model, - type: 'Collection' - }).items; - const items = itemsProp !== undefined ? itemsProp : myViewDataStore; - - if custom enabled - const myViewDataStore = []; - const items = itemsProp !== undefined ? itemsProp : myViewDataStore; - */ - statements.push( - this.buildCollectionBindingCall(), - factory.createVariableStatement( - undefined, - factory.createVariableDeclarationList( - [ - factory.createVariableDeclaration( - factory.createIdentifier('items'), - undefined, - undefined, - factory.createConditionalExpression( - factory.createBinaryExpression( + if (isDataStoreEnabled) { + /** + * builds predicate variable + */ + if (predicate) { + this.importCollection.addMappedImport(ImportValue.CREATE_DATA_STORE_PREDICATE); + statements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + getFilterName(model), + undefined, + undefined, + this.predicateToObjectLiteralExpression(predicate), + ), + ], + NodeFlags.Const, + ), + ), + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + getPredicateName(model), + undefined, + undefined, + factory.createCallExpression( + factory.createIdentifier('createDataStorePredicate'), + [factory.createTypeReferenceNode(factory.createIdentifier(model), undefined)], + [factory.createIdentifier(getFilterName(model))], + ), + ), + ], + NodeFlags.Const, + ), + ), + ); + } + /** + * builds sort function + */ + if (sort) { + this.importCollection.addMappedImport(ImportValue.SORT_DIRECTION); + this.importCollection.addMappedImport(ImportValue.SORT_PREDICATE); + statements.push( + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + getPaginationName(model), + undefined, + undefined, + factory.createObjectLiteralExpression([ + factory.createPropertyAssignment(factory.createIdentifier('sort'), buildSortFunction(model, sort)), + ]), + ), + ], + NodeFlags.Const, + ), + ), + ); + } + /* + if datastore enabled + const myViewDataStore = useDataStoreBinding({ + model: Model, + type: 'Collection' + }).items; + const items = itemsProp !== undefined ? itemsProp : myViewDataStore; + + if custom enabled + uses regular items array for formatting + */ + const dsItemsName = factory.createIdentifier(`${this.viewComponent.name}DataStore`); + statements.push( + buildBaseCollectionVariableStatement( + dsItemsName, + buildDataStoreCollectionCall( + model, + predicate ? getPredicateName(model) : undefined, + sort ? getPaginationName(model) : undefined, + ), + ), + // checks to see if an override was passed + factory.createVariableStatement( + undefined, + factory.createVariableDeclarationList( + [ + factory.createVariableDeclaration( + factory.createIdentifier('items'), + undefined, + undefined, + factory.createConditionalExpression( + factory.createBinaryExpression( + factory.createIdentifier('itemsProp'), + factory.createToken(SyntaxKind.ExclamationEqualsEqualsToken), + factory.createIdentifier('undefined'), + ), + factory.createToken(SyntaxKind.QuestionToken), factory.createIdentifier('itemsProp'), - factory.createToken(SyntaxKind.ExclamationEqualsEqualsToken), - factory.createIdentifier('undefined'), + factory.createToken(SyntaxKind.ColonToken), + dsItemsName, ), - factory.createToken(SyntaxKind.QuestionToken), - factory.createIdentifier('itemsProp'), - factory.createToken(SyntaxKind.ColonToken), - factory.createIdentifier('itemsDataStore'), ), - ), - ], - NodeFlags.Const, + ], + NodeFlags.Const, + ), ), - ), - ); + ); + } return statements; } - private buildCollectionBindingCall() { - const { type, model } = this.viewComponent.dataSource; - const itemsName = `${this.viewComponent.name}DataStore`; - if (type === 'DataStore' && model) { - this.importCollection.addMappedImport(ImportValue.USE_DATA_STORE_BINDING); - const objectProperties = [ - factory.createPropertyAssignment(factory.createIdentifier('type'), factory.createStringLiteral('collection')), - factory.createPropertyAssignment(factory.createIdentifier('model'), factory.createIdentifier(model)), - ]; - - const callExp = factory.createCallExpression(factory.createIdentifier('useDataStoreBinding'), undefined, [ - factory.createObjectLiteralExpression(objectProperties, true), - ]); - return buildBaseCollectionVariableStatement(factory.createIdentifier(itemsName), callExp); - } - return factory.createVariableStatement( - undefined, - factory.createVariableDeclarationList( - [ - factory.createVariableDeclaration( - factory.createIdentifier(itemsName), - undefined, - undefined, - factory.createArrayLiteralExpression([], false), - ), - ], - NodeFlags.Const, - ), + private predicateToObjectLiteralExpression(predicate: StudioComponentPredicate): ObjectLiteralExpression { + return factory.createObjectLiteralExpression( + Object.entries(predicate).map(([key, value]) => { + return factory.createPropertyAssignment( + factory.createIdentifier(key), + key === 'and' || key === 'or' + ? factory.createArrayLiteralExpression( + (value as StudioComponentPredicate[]).map( + (pred: StudioComponentPredicate) => this.predicateToObjectLiteralExpression(pred), + false, + ), + ) + : factory.createStringLiteral(value as string), + ); + }, false), ); } diff --git a/packages/codegen-ui/example-schemas/datastore/post-ds.json b/packages/codegen-ui/example-schemas/datastore/post-ds.json new file mode 100644 index 000000000..61e902db5 --- /dev/null +++ b/packages/codegen-ui/example-schemas/datastore/post-ds.json @@ -0,0 +1,106 @@ +{ + "models": { + "Post": { + "name": "Post", + "fields": { + "id": { + "name": "id", + "isArray": false, + "type": "ID", + "isRequired": true, + "attributes": [] + }, + "caption": { + "name": "caption", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "username": { + "name": "username", + "isArray": false, + "type": "String", + "isRequired": false, + "attributes": [] + }, + "post_url": { + "name": "post_url", + "isArray": false, + "type": "AWSURL", + "isRequired": false, + "attributes": [] + }, + "profile_url": { + "name": "profile_url", + "isArray": false, + "type": "AWSURL", + "isRequired": false, + "attributes": [] + }, + "status": { + "name": "status", + "isArray": false, + "type": { + "enum": "PostStatus" + }, + "isRequired": false, + "attributes": [] + }, + "createdAt": { + "name": "createdAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + }, + "updatedAt": { + "name": "updatedAt", + "isArray": false, + "type": "AWSDateTime", + "isRequired": false, + "attributes": [], + "isReadOnly": true + } + }, + "syncable": true, + "pluralName": "Posts", + "attributes": [ + { + "type": "model", + "properties": {} + }, + { + "type": "auth", + "properties": { + "rules": [ + { + "allow": "private", + "provider": "iam", + "operations": [ + "create", + "update", + "delete", + "read" + ] + } + ] + } + } + ] + } + }, + "enums": { + "PostStatus": { + "name": "PostStatus", + "values": [ + "PENDING", + "POSTED", + "IN_REVIEW" + ] + } + }, + "nonModels": {}, + "version": "00000" +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/views/post-table-custom-format.json b/packages/codegen-ui/example-schemas/views/post-table-custom-format.json new file mode 100644 index 000000000..851694a8c --- /dev/null +++ b/packages/codegen-ui/example-schemas/views/post-table-custom-format.json @@ -0,0 +1,45 @@ +{ + "dataSource": { + "model": "Post", + "predicate": { + "and": [ + { + "field": "username", + "operand": "Guy", + "operator": "notContains" + }, + { + "field": "createdAt", + "operand": "25", + "operator": "contains" + } + ] + }, + "sort": [ + { + "direction": "ASC", + "field": "username" + } + ], + "type": "DataStore" + }, + "id": "v000001", + "name": "MyPostTable", + "schemaVersion": "1.0.0", + "style": {}, + "viewConfiguration": { + "columns": { + "createdAt": { + "valueFormatting": { + "stringFormat": { + "dateTimeFormat": { + "dateFormat": "MM/DD/YYYY", + "timeFormat": "locale" + } + } + } + } + }, + "type": "Table" + } +} \ No newline at end of file diff --git a/packages/codegen-ui/example-schemas/views/post-table-datastore.json b/packages/codegen-ui/example-schemas/views/post-table-datastore.json new file mode 100644 index 000000000..ff769eba4 --- /dev/null +++ b/packages/codegen-ui/example-schemas/views/post-table-datastore.json @@ -0,0 +1,31 @@ +{ + "dataSource": { + "model": "Post", + "predicate": { + "and": [ + { + "field": "username", + "operand": "username0", + "operator": "notContains" + }, + { + "field": "createdAt", + "operand": "2022", + "operator": "contains" + } + ] + }, + "sort": [ + { + "direction": "ASC", + "field": "username" + } + ], + "type": "DataStore" + }, + "id": "v-0001", + "name": "MyPostTable", + "schemaVersion": "1.0.0", + "style": {}, + "viewConfiguration": { "type": "Table" } +} \ No newline at end of file diff --git a/packages/codegen-ui/lib/types/view/view-metadata.ts b/packages/codegen-ui/lib/types/view/view-metadata.ts index 674687845..e2ff96d2f 100644 --- a/packages/codegen-ui/lib/types/view/view-metadata.ts +++ b/packages/codegen-ui/lib/types/view/view-metadata.ts @@ -20,5 +20,5 @@ export type ViewMetadata = { id?: string; name: string; // Stores the configured formatting for each field (table column) - tableFieldFormatting?: { [fieldName: string]: ViewValueFormatting }; + fieldFormatting: { [fieldName: string]: ViewValueFormatting }; }; diff --git a/packages/test-generator/lib/generators/BrowserTestGenerator.ts b/packages/test-generator/lib/generators/BrowserTestGenerator.ts index 110b2e0e3..b5d83a67f 100644 --- a/packages/test-generator/lib/generators/BrowserTestGenerator.ts +++ b/packages/test-generator/lib/generators/BrowserTestGenerator.ts @@ -28,7 +28,7 @@ import { ReactIndexStudioTemplateRenderer, ReactUtilsStudioTemplateRenderer, AmplifyFormRenderer, - Util, + UtilTemplateType, } from '@aws-amplify/codegen-ui-react'; import schema from '../models/schema'; import { TestGenerator } from './TestGenerator'; @@ -52,7 +52,7 @@ export class BrowserTestGenerator extends TestGenerator { return new ReactIndexStudioTemplateRenderer(schemas, this.renderConfig).renderComponent(); } - renderUtilsFile(utils: Util[]) { + renderUtilsFile(utils: UtilTemplateType[]) { return new ReactUtilsStudioTemplateRenderer(utils, this.renderConfig).renderComponent(); } diff --git a/packages/test-generator/lib/generators/NodeTestGenerator.ts b/packages/test-generator/lib/generators/NodeTestGenerator.ts index 1f0620367..8c2e89d4d 100644 --- a/packages/test-generator/lib/generators/NodeTestGenerator.ts +++ b/packages/test-generator/lib/generators/NodeTestGenerator.ts @@ -32,7 +32,7 @@ import { ReactIndexStudioTemplateRenderer, ReactUtilsStudioTemplateRenderer, AmplifyFormRenderer, - Util, + UtilTemplateType, } from '@aws-amplify/codegen-ui-react'; import schema from '../models/schema'; import { TestGenerator, TestGeneratorParams } from './TestGenerator'; @@ -74,7 +74,7 @@ export class NodeTestGenerator extends TestGenerator { (schemas: StudioSchema[]) => new ReactIndexStudioTemplateRenderer(schemas, this.renderConfig), ); this.utilsRendererFactory = new StudioTemplateRendererFactory( - (utils: Util[]) => new ReactUtilsStudioTemplateRenderer(utils, this.renderConfig), + (utils: UtilTemplateType[]) => new ReactUtilsStudioTemplateRenderer(utils, this.renderConfig), ); this.componentRendererManager = new StudioTemplateRendererManager(this.componentRendererFactory, this.outputConfig); this.formRendererManager = new StudioTemplateRendererManager(this.formRendererFactory, this.outputConfig);