Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Future Admin Generator: parse config using ts-morph to support inline functions and direct imports #3297

Open
wants to merge 25 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
baa70ae
Future Admin Generator PoC: parse config using ts-morph to support in…
nsams Feb 15, 2025
5096614
Add support for variable declaration with initializer
nsams Feb 14, 2025
22a0e09
Implement grid renderCell defined as arrow function
nsams Feb 15, 2025
6bfb78a
Add support for using imported components (or anything else) in rende…
nsams Feb 14, 2025
8977d18
Add unit tests for tsMorphHelpers
nsams Feb 14, 2025
04b25ea
Remove support for explicit {name, import} config, support new import…
nsams Feb 15, 2025
81b5033
eslint
nsams Feb 15, 2025
e2c96e0
Restrict parsing import/inline-code to paths where we have support fo…
nsams Feb 20, 2025
14b6ae0
Fix merge errors an others
nsams Feb 20, 2025
651e400
re-add accidentally removed file
nsams Feb 20, 2025
69c8161
update ts-morph to latest version
nsams Feb 20, 2025
07c9dd5
Fix import
nsams Feb 20, 2025
f07a811
Merge branch 'next' into admingen-tsmorph
nsams Feb 20, 2025
24608cd
convert merged cometGen to new api
nsams Feb 21, 2025
e2d25ba
quick workaround for nested fields
nsams Feb 24, 2025
0687518
Don't export internal function
nsams Feb 25, 2025
75fb2c5
knip: cometGen now can also be tsx
nsams Feb 25, 2025
74332ce
Use type from final-from for validate function
nsams Feb 27, 2025
d68b1ec
Improve typing: Type Guard used by generator runtime for places where…
nsams Feb 27, 2025
0b9d639
Update comment, that's not a todo anymore
nsams Feb 27, 2025
7733d9c
Add poor-mans recursion support for nested fields with up to 5 levels…
nsams Feb 27, 2025
174a6cc
merge next into admingen-tsmorph
manuelblum Feb 27, 2025
e89e799
format: pre.json
manuelblum Feb 27, 2025
6055350
Fix wrong type in supportedImportPaths
nsams Feb 28, 2025
a4bcf14
fix knip issues
nsams Feb 28, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pnpm exec lint-staged

8 changes: 5 additions & 3 deletions demo/admin/src/news/NewsForm.cometGen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { type future_FormConfig as FormConfig } from "@comet/cms-admin";
import { type DamImageBlock, type future_FormConfig as FormConfig } from "@comet/cms-admin";
import { type GQLNews } from "@src/graphql.generated";

import { NewsContentBlock } from "./blocks/NewsContentBlock";

export const NewsForm: FormConfig<GQLNews> = {
type: "form",
gqlType: "News",
Expand Down Expand Up @@ -36,13 +38,13 @@ export const NewsForm: FormConfig<GQLNews> = {
type: "block",
name: "image",
label: "Image",
block: { name: "DamImageBlock", import: "@comet/cms-admin" },
block: DamImageBlock,
},
{
type: "block",
name: "content",
label: "Content",
block: { name: "NewsContentBlock", import: "../blocks/NewsContentBlock" },
block: NewsContentBlock,
},
],
};
8 changes: 5 additions & 3 deletions demo/admin/src/news/NewsGrid.cometGen.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { type future_GridConfig as GridConfig } from "@comet/cms-admin";
import { type DamImageBlock, type future_GridConfig as GridConfig } from "@comet/cms-admin";
import { type GQLNews } from "@src/graphql.generated";

import { NewsContentBlock } from "./blocks/NewsContentBlock";

export const NewsGrid: GridConfig<GQLNews> = {
type: "grid",
gqlType: "News",
Expand All @@ -26,13 +28,13 @@ export const NewsGrid: GridConfig<GQLNews> = {
type: "block",
name: "image",
headerName: "Image",
block: { name: "DamImageBlock", import: "@comet/cms-admin" },
block: DamImageBlock,
},
{
type: "block",
name: "content",
headerName: "Content",
block: { name: "NewsContentBlock", import: "../blocks/NewsContentBlock" },
block: NewsContentBlock,
},
],
};
11 changes: 8 additions & 3 deletions demo/admin/src/products/ProductsGridPreviewAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,17 @@ import {
Typography,
} from "@mui/material";
import { type GridCellParams, type GridValidRowModel } from "@mui/x-data-grid-pro";
import { type GQLProductsGridFutureFragment } from "@src/products/future/generated/ProductsGrid.generated";
import { type GQLProductsListManualFragment } from "@src/products/ProductsGrid.generated";
import { FormattedMessage } from "react-intl";

import { Dialog, DialogContent, DialogTitle, IconButton, Typography } from "@mui/material";
import { GridCellParams } from "@mui/x-data-grid-pro";
import { useState } from "react";
import { FormattedMessage } from "react-intl";

type Props = GridCellParams<GridValidRowModel, GQLProductsListManualFragment | GQLProductsGridFutureFragment>;
import { GQLProductsGridFutureFragment } from "./future/generated/ProductsGrid.generated";
import { GQLProductsListManualFragment } from "./ProductsGrid.generated";

type Props = GridCellParams<unknown, GQLProductsListManualFragment | GQLProductsGridFutureFragment>;

export const ProductsGridPreviewAction = ({ row }: Props) => {
const [showDetails, setShowDetails] = useState(false);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { type future_FormConfig as FormConfig } from "@comet/cms-admin";
import { type DamImageBlock, type future_FormConfig as FormConfig } from "@comet/cms-admin";
import { type GQLProduct } from "@src/graphql.generated";

import { validateTitle } from "./validateTitle";

export const CreateCapProductForm: FormConfig<GQLProduct> = {
type: "form",
gqlType: "Product",
Expand All @@ -12,13 +14,13 @@ export const CreateCapProductForm: FormConfig<GQLProduct> = {
name: "title",
label: "Titel", // default is generated from name (camelCaseToHumanReadable)
required: true, // default is inferred from gql schema
validate: { name: "validateTitle", import: "./validateTitle" },
validate: validateTitle,
},
{ type: "text", name: "slug" },
{ type: "text", name: "description", label: "Description", multiline: true },
{ type: "asyncSelect", name: "category", rootQuery: "productCategories" },
{ type: "boolean", name: "inStock" },
{ type: "date", name: "availableSince", startAdornment: { icon: "CalendarToday" } },
{ type: "block", name: "image", label: "Image", block: { name: "DamImageBlock", import: "@comet/cms-admin" } },
{ type: "block", name: "image", label: "Image", block: DamImageBlock },
],
};
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { type future_FormConfig as FormConfig } from "@comet/cms-admin";
import { DamImageBlock, type future_FormConfig as FormConfig } from "@comet/cms-admin";
import { type GQLProduct } from "@src/graphql.generated";
import { FormattedMessage } from "react-intl";

import { FutureProductNotice } from "../helpers/FutureProductNotice";

export const ProductForm: FormConfig<GQLProduct> = {
type: "form",
Expand All @@ -18,7 +21,10 @@ export const ProductForm: FormConfig<GQLProduct> = {
name: "title",
label: "Titel", // default is generated from name (camelCaseToHumanReadable)
required: true, // default is inferred from gql schema
validate: { name: "validateTitle", import: "./validateTitle" },
validate: (value: string) =>
value.length < 3 ? (
<FormattedMessage id="product.validate.titleMustBe3CharsLog" defaultMessage="Title must be at least 3 characters long" />
) : undefined,
},
{ type: "text", name: "slug" },
{ type: "date", name: "createdAt", label: "Created", readOnly: true },
Expand Down Expand Up @@ -68,8 +74,8 @@ export const ProductForm: FormConfig<GQLProduct> = {
},
{ type: "boolean", name: "inStock" },
{ type: "date", name: "availableSince", startAdornment: { icon: "CalendarToday" } },
{ type: "component", component: { name: "FutureProductNotice", import: "../../helpers/FutureProductNotice" } },
{ type: "block", name: "image", label: "Image", block: { name: "DamImageBlock", import: "@comet/cms-admin" } },
{ type: "component", component: FutureProductNotice },
{ type: "block", name: "image", label: "Image", block: DamImageBlock },
{ type: "fileUpload", name: "priceList", label: "Price List", maxFileSize: 1024 * 1024 * 4, download: true },
{ type: "fileUpload", name: "datasheets", label: "Datasheets", multiple: true, maxFileSize: 1024 * 1024 * 4, download: false },
{ type: "dateTime", name: "lastCheckedAt", label: "Last checked at" },
Expand Down
3 changes: 3 additions & 0 deletions demo/admin/src/products/future/ProductTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function ProductTitle({ title }: { title: string }) {
return <div>Product: {title}</div>;
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { type future_GridConfig as GridConfig } from "@comet/cms-admin";
import { type GQLProduct } from "@src/graphql.generated";

import { ProductsGridPreviewAction } from "../ProductsGridPreviewAction";
import { ManufacturerFilterOperators } from "./ManufacturerFilter";
import { ProductTitle } from "./ProductTitle";

const typeValues = [{ value: "Cap", label: "great Cap" }, "Shirt", "Tie"];

export const ProductsGrid: GridConfig<GQLProduct> = {
Expand Down Expand Up @@ -62,6 +66,14 @@ export const ProductsGrid: GridConfig<GQLProduct> = {
sortBy: ["title", "price", "type", "category", "inStock"],
disableExport: true, // TODO: Implement `valueFormatter` for type "combination"
},
{
type: "text",
renderCell: ({ value, row }) => <ProductTitle title={value} />,
name: "title",
headerName: "Custom",
minWidth: 200,
visible: "down('md')",
},
{ type: "text", name: "title", headerName: "Titel", minWidth: 200, maxWidth: 250, visible: "up('md')" },
{ type: "text", name: "description", headerName: "Description" },
// TODO: Allow setting options for `intl.formatNumber` through `valueFormatter` (type "number")
Expand Down Expand Up @@ -102,11 +114,11 @@ export const ProductsGrid: GridConfig<GQLProduct> = {
name: "manufacturer.name",
headerName: "Manufacturer",
fieldName: "manufacturer",
filterOperators: { name: "ManufacturerFilterOperators", import: "./ManufacturerFilter" },
filterOperators: ManufacturerFilterOperators,
},
{
type: "actions",
component: { name: "ProductsGridPreviewAction", import: "../../ProductsGridPreviewAction" },
component: ProductsGridPreviewAction,
},
],
};
14 changes: 10 additions & 4 deletions demo/admin/src/products/future/generated/ProductForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ import { FormSpy } from "react-final-form";
import { FormattedMessage } from "react-intl";

import { FutureProductNotice } from "../../helpers/FutureProductNotice";
import { validateTitle } from "../validateTitle";
import {
type GQLManufacturersSelectQuery,
type GQLManufacturersSelectQueryVariables,
Expand Down Expand Up @@ -213,7 +212,14 @@ export function ProductForm({ id }: FormProps) {
fullWidth
name="title"
label={<FormattedMessage id="product.title" defaultMessage="Titel" />}
validate={validateTitle}
validate={(value: string) =>
value.length < 3 ? (
<FormattedMessage
id="product.validate.titleMustBe3CharsLog"
defaultMessage="Title must be at least 3 characters long"
/>
) : undefined
}
/>

<TextField
Expand Down Expand Up @@ -423,14 +429,14 @@ export function ProductForm({ id }: FormProps) {
name="priceList"
label={<FormattedMessage id="product.priceList" defaultMessage="Price List" />}
variant="horizontal"
maxFileSize={4194304}
maxFileSize={1024 * 1024 * 4}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here we also can keep the non-evaluated version (good)

/>
<FileUploadField
name="datasheets"
label={<FormattedMessage id="product.datasheets" defaultMessage="Datasheets" />}
variant="horizontal"
multiple
maxFileSize={4194304}
maxFileSize={1024 * 1024 * 4}
/>
<DateTimeField
variant="horizontal"
Expand Down
9 changes: 9 additions & 0 deletions demo/admin/src/products/future/generated/ProductsGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";

import { ProductsGridPreviewAction } from "../../ProductsGridPreviewAction";
import { ManufacturerFilterOperators } from "../ManufacturerFilter";
import { ProductTitle } from "../ProductTitle";
import {
type GQLCreateProductMutation,
type GQLCreateProductMutationVariables,
Expand Down Expand Up @@ -218,6 +219,14 @@ export function ProductsGrid({ filter, toolbarAction, rowAction, actionsColumnWi
sortBy: ["title", "price", "type", "category", "inStock"],
minWidth: 200,
},
{
field: "title",
headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Custom" }),
renderCell: ({ value, row }) => <ProductTitle title={value} />,
flex: 1,
visible: theme.breakpoints.down("md"),
minWidth: 200,
},
{
field: "title",
headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Titel" }),
Expand Down
1 change: 1 addition & 0 deletions packages/admin/cms-admin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@
"rimraf": "^6.0.1",
"scroll-into-view-if-needed": "^3.1.0",
"slugify": "^1.6.6",
"ts-morph": "^16.0.0",
"use-debounce": "^10.0.4",
"uuid": "^11.0.5"
},
Expand Down
9 changes: 5 additions & 4 deletions packages/admin/cms-admin/src/generator/future/generateForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
isFormFieldConfig,
isFormLayoutConfig,
} from "./generator";
import { convertConfigImport } from "./utils/convertConfigImport";
import { findMutationTypeOrThrow } from "./utils/findMutationType";
import { generateImportsCode, type Imports } from "./utils/generateImportsCode";

Expand Down Expand Up @@ -108,10 +109,10 @@ export function generateForm(
return field;
});
rootBlockFields.forEach((field) => {
imports.push({
name: field.block.name,
importPath: field.block.import,
});
if ("import" in field.block) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
imports.push(convertConfigImport(field.block as any)); // TODO: improve typing, generator runtime vs. config mismatch
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When do you plan to do this?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have an idea yet how this could be solved. We can talk about it today.

}
});

const readOnlyFields = formFields.filter((field) => field.readOnly);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { type ComponentFormFieldConfig } from "../generator";
import { type Imports } from "../utils/generateImportsCode";
import { convertConfigImport } from "../utils/convertConfigImport";
import { type GenerateFieldsReturn } from "./generateFields";

export function generateComponentFormField({ config }: { config: ComponentFormFieldConfig }): GenerateFieldsReturn {
const imports: Imports = [{ name: config.component.name, importPath: config.component.import }];
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const imports = [convertConfigImport(config.component as any)]; // TODO: improve typing, generator runtime vs. config mismatch
const code = `<${config.component.name} />`;

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {

import { type Adornment, type FormConfig, type FormFieldConfig, type GQLDocumentConfigMap, isFormFieldConfig } from "../generator";
import { camelCaseToHumanReadable } from "../utils/camelCaseToHumanReadable";
import { convertConfigImport } from "../utils/convertConfigImport";
import { findQueryTypeOrThrow } from "../utils/findQueryType";
import { type Imports } from "../utils/generateImportsCode";
import { isFieldOptional } from "../utils/isFieldOptional";
Expand Down Expand Up @@ -35,10 +36,8 @@ const getAdornmentData = ({ adornmentData }: { adornmentData: Adornment }): Ador
} else if (typeof adornmentData.icon === "object") {
if ("import" in adornmentData.icon) {
adornmentString = `<${adornmentData.icon.name} />`;
adornmentImport = {
name: `${adornmentData.icon.name}`,
importPath: `${adornmentData.icon.import}`,
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
adornmentImport = convertConfigImport(adornmentData.icon as any); // TODO: improve typing, generator runtime vs. config mismatch
} else {
const { name, ...iconProps } = adornmentData.icon;
adornmentString = `<${name}Icon
Expand Down Expand Up @@ -137,16 +136,13 @@ export function generateFormField({

let validateCode = "";
if (config.validate) {
let importPath = config.validate.import;
if (importPath.startsWith("./")) {
//go one level up as generated files are in generated subfolder
importPath = `.${importPath}`;
if ("import" in config.validate) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
imports.push(convertConfigImport(config.validate as any)); // TODO: improve typing, generator runtime vs. config mismatch
validateCode = `validate={${config.validate.name}}`;
} else if ("code" in config.validate) {
validateCode = `validate={${config.validate.code}}`;
}
imports.push({
name: config.validate.name,
importPath,
});
validateCode = `validate={${config.validate.name}}`;
}

const fieldLabel = `<FormattedMessage id="${formattedMessageRootId}.${name}" defaultMessage="${label}" />`;
Expand Down
Loading
Loading