UNPKG

@baseplate-dev/react-generators

Version:

React Generators for Baseplate

290 lines (289 loc) 15.5 kB
import { tsCodeFragment, TsCodeUtils, tsHoistedFragment, tsTemplate, tsTypeImportBuilder, typescriptFileProvider, } from '@baseplate-dev/core-generators'; import { createGenerator, createGeneratorTask, createProviderType, createReadOnlyProviderType, } from '@baseplate-dev/sync'; import { notEmpty } from '@baseplate-dev/utils'; import { posixJoin } from '@baseplate-dev/utils/node'; import { kebabCase, sortBy } from 'es-toolkit'; import { z } from 'zod'; import { reactComponentsImportsProvider } from '#src/generators/core/react-components/index.js'; import { reactErrorImportsProvider } from '#src/generators/core/react-error/index.js'; import { upperCaseFirst } from '#src/utils/case.js'; import { adminCrudColumnContainerProvider } from '../_providers/admin-crud-column-container.js'; import { adminCrudInputContainerProvider } from '../_providers/admin-crud-input-container.js'; import { getPassthroughExtraProps, mergeAdminCrudDataDependencies, } from '../_utils/data-loaders.js'; import { adminComponentsImportsProvider } from '../admin-components/index.js'; import { adminCrudEditProvider } from '../admin-crud-edit/index.js'; import { adminCrudSectionScope } from '../admin-crud-section/index.js'; import { ADMIN_ADMIN_CRUD_EMBEDDED_FORM_GENERATED } from './generated/index.js'; const descriptorSchema = z.object({ id: z.string(), name: z.string(), isList: z.boolean(), modelName: z.string(), idField: z.string().optional(), }); export const adminCrudEmbeddedFormProvider = createProviderType('admin-crud-embedded-form', { isReadOnly: true }); function getComponentProps({ inputType, componentType, formDataType, dataDependencies, adminComponentsImports, }) { const defaultProps = `Embedded${inputType}${componentType}Props`; // actually not sure why it's not supported here but it doesn't exist if (defaultProps === 'EmbeddedObjectTableProps') { throw new Error('EmbeddedObjectTableProps is not supported'); } const defaultPropsImport = adminComponentsImports[defaultProps].typeFragment(); const defaultPropsExpression = tsTemplate `${defaultPropsImport}<${formDataType}>`; if (dataDependencies.length === 0) { return defaultPropsExpression; } const propsName = `${componentType}Props`; return tsCodeFragment(propsName, undefined, { hoistedFragments: [ tsHoistedFragment(propsName, TsCodeUtils.formatFragment(` interface PROPS_NAME extends DEFAULT_PROPS { INTERFACE_CONTENT }`, { PROPS_NAME: propsName, DEFAULT_PROPS: defaultPropsExpression, INTERFACE_CONTENT: TsCodeUtils.mergeFragmentsAsInterfaceContent(Object.fromEntries(dataDependencies.map((d) => [ d.propName, d.propType, ]))), })), ], }); } const adminCrudEmbeddedFormSetupProvider = createReadOnlyProviderType('admin-crud-embedded-form-setup'); export const adminCrudEmbeddedFormGenerator = createGenerator({ name: 'admin/admin-crud-embedded-form', generatorFileUrl: import.meta.url, descriptorSchema, getInstanceName: (descriptor) => descriptor.id, buildTasks: ({ id, name, modelName, isList, idField }) => ({ setupForm: createGeneratorTask({ dependencies: {}, exports: { adminCrudInputContainer: adminCrudInputContainerProvider.export(), adminCrudColumnContainer: adminCrudColumnContainerProvider.export(), }, outputs: { adminCrudEmbeddedFormSetup: adminCrudEmbeddedFormSetupProvider.export(), }, run() { const inputFields = []; const tableColumns = []; return { providers: { adminCrudInputContainer: { addInput: (input) => inputFields.push(input), getModelName: () => modelName, isInModal: () => true, }, adminCrudColumnContainer: { addColumn: (column) => { if (!isList) { throw new Error('Cannot add columns to a non-list embedded form'); } tableColumns.push(column); }, getModelName: () => modelName, }, }, build: () => ({ adminCrudEmbeddedFormSetup: { inputFields: inputFields.sort((a, b) => a.order - b.order), tableColumns: tableColumns.sort((a, b) => a.order - b.order), }, }), }; }, }), main: createGeneratorTask({ dependencies: { adminCrudEdit: adminCrudEditProvider, adminComponentsImports: adminComponentsImportsProvider, reactComponentsImports: reactComponentsImportsProvider, reactErrorImports: reactErrorImportsProvider, typescriptFile: typescriptFileProvider, adminCrudEmbeddedFormSetup: adminCrudEmbeddedFormSetupProvider, }, exports: { adminCrudEmbeddedForm: adminCrudEmbeddedFormProvider.export(adminCrudSectionScope, id), }, run({ adminCrudEdit, reactComponentsImports, reactErrorImports, typescriptFile, adminComponentsImports, adminCrudEmbeddedFormSetup: { inputFields, tableColumns }, }) { const capitalizedName = upperCaseFirst(name); const formName = `Embedded${capitalizedName}Form`; const formDataType = `Embedded${capitalizedName}FormData`; const formSchema = `embedded${capitalizedName}FormSchema`; const formPath = posixJoin(adminCrudEdit.getDirectoryBase(), `-components/${kebabCase(formName)}.tsx`); const inputDataDependencies = inputFields.flatMap((f) => f.dataDependencies ?? []); const tableName = `Embedded${capitalizedName}Table`; const tableDataDependencies = tableColumns.flatMap((f) => f.display.dataDependencies ?? []); const allDataDependencies = mergeAdminCrudDataDependencies([ ...inputDataDependencies, ...tableDataDependencies, ]); const graphQLFields = [ ...(idField ? [{ name: idField }] : []), ...inputFields.flatMap((f) => f.graphQLFields), ...tableColumns.flatMap((f) => f.display.graphQLFields), ]; // Create schema const validations = [ ...(idField && !inputFields.some((f) => f.validation.some((v) => v.key === idField)) ? [ { key: idField, // TODO: Allow non-string IDs expression: tsCodeFragment('z.string().nullish()'), }, ] : []), ...inputFields.flatMap((f) => f.validation), ]; const embeddedBlock = tsHoistedFragment(formSchema, TsCodeUtils.formatFragment(` export const TPL_SCHEMA_NAME = z.object(TPL_SCHEMA_OBJECT); export type TPL_SCHEMA_TYPE = z.infer<typeof TPL_SCHEMA_NAME>; `, { TPL_SCHEMA_NAME: formSchema, TPL_SCHEMA_TYPE: formDataType, TPL_SCHEMA_OBJECT: TsCodeUtils.mergeFragmentsAsObject(Object.fromEntries(validations.map((v) => [v.key, v.expression]))), })); const validationExpression = tsCodeFragment(isList ? `z.array(${formSchema})` : formSchema, undefined, { hoistedFragments: [embeddedBlock] }); return { providers: { adminCrudEmbeddedForm: { getEmbeddedFormInfo: () => { const sharedData = { embeddedFormComponent: { expression: TsCodeUtils.importFragment(formName, formPath), extraProps: getPassthroughExtraProps(inputDataDependencies), }, dataDependencies: allDataDependencies, graphQLFields, validationExpression, }; if (isList) { return { type: 'list', ...sharedData, embeddedTableComponent: { expression: TsCodeUtils.importFragment(tableName, formPath), extraProps: getPassthroughExtraProps(tableDataDependencies), }, }; } return { type: 'object', ...sharedData, }; }, }, }, build: async (builder) => { const tableImports = [ reactComponentsImports.Table.declaration(), reactComponentsImports.TableHeader.declaration(), reactComponentsImports.TableHead.declaration(), reactComponentsImports.TableBody.declaration(), reactComponentsImports.TableRow.declaration(), reactComponentsImports.TableCell.declaration(), ]; const headers = tableColumns.map((column) => tsCodeFragment(`<TableHead>${column.label}</TableHead>`)); const cells = tableColumns.map((column) => tsTemplate `<TableCell>${column.display.content('item')}</TableCell>`); const tableComponent = isList ? TsCodeUtils.formatFragment(` export function TPL_COMPONENT_NAME({ items, edit, remove, TPL_EXTRA_PROP_SPREAD }: TPL_PROPS): ReactElement { return ( <Table className="max-w-6xl"> <TableHeader> <TableRow> TPL_HEADERS <TableCell>Actions</TableCell> </TableRow> </TableHeader> <TableBody> {items.map((item, idx) => ( <TableRow key={item.id}> TPL_CELLS <TableCell className="space-x-4"> <Button variant="link" onClick={() => { edit(idx); }}>Edit</Button> <Button variant="linkDestructive" onClick={() => { remove(idx); }}> Remove </Button> </TableCell> </TableRow> ))} </TableBody> </Table> ); } `, { TPL_COMPONENT_NAME: tableName, TPL_EXTRA_PROP_SPREAD: tableDataDependencies .map((d) => d.propName) .join(',\n'), TPL_PROPS: getComponentProps({ inputType: 'List', componentType: 'Table', formDataType, dataDependencies: tableDataDependencies, adminComponentsImports, }), TPL_HEADERS: TsCodeUtils.mergeFragmentsPresorted(headers, '\n'), TPL_CELLS: TsCodeUtils.mergeFragmentsPresorted(cells, '\n'), }, [ ...tableImports, reactComponentsImports.Button.declaration(), tsTypeImportBuilder(['ReactElement']).from('react'), ]) : tsCodeFragment(''); const sortedInputFields = sortBy(inputFields, [(f) => f.order]); await builder.apply(typescriptFile.renderTemplateFile({ id: `embedded-form-${id}`, template: ADMIN_ADMIN_CRUD_EMBEDDED_FORM_GENERATED.templates .embeddedForm, destination: formPath, variables: { TPL_EMBEDDED_FORM_DATA_TYPE: TsCodeUtils.typeImportFragment(formDataType, adminCrudEdit.getSchemaImport()), TPL_EMBEDDED_FORM_DATA_SCHEMA: TsCodeUtils.importFragment(formSchema, adminCrudEdit.getSchemaImport()), TPL_COMPONENT_NAME: formName, TPL_INPUTS: TsCodeUtils.mergeFragmentsPresorted(sortedInputFields.map((input) => input.content), '\n'), TPL_HEADER: TsCodeUtils.mergeFragmentsPresorted(sortedInputFields .map((field) => field.header) .filter(notEmpty)), TPL_DESTRUCTURED_PROPS: `{ initialData, onSubmit, ${inputDataDependencies .map((d) => d.propName) .join(',\n')} }`, TPL_PROPS: getComponentProps({ inputType: isList ? 'List' : 'Object', componentType: 'Form', formDataType, dataDependencies: inputDataDependencies, adminComponentsImports, }), TPL_TABLE_COMPONENT: tableComponent, }, importMapProviders: { reactComponentsImports, reactErrorImports, }, })); }, }; }, }), }), }); //# sourceMappingURL=admin-crud-embedded-form.generator.js.map