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

Admin-Generator: CRUD Page generator #3573

Draft
wants to merge 1 commit into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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 demo/admin/src/common/MasterMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import { categoryToUrlParam, pageTreeCategories, urlParamToCategory } from "@src
import ProductCategoriesPage from "@src/products/categories/ProductCategoriesPage";
import { CombinationFieldsTestProductsPage } from "@src/products/future/CombinationFieldsTestProductsPage";
import { CreateCapProductPage as FutureCreateCapProductPage } from "@src/products/future/CreateCapProductPage";
import { ManufacturersPage as FutureManufacturersPage } from "@src/products/future/ManufacturersPage";
import { ManufacturersPage as FutureManufacturersPage } from "@src/products/future/generated/ManufacturersPage";
import { ProductCategoriesHandmadePage } from "@src/products/future/ProductCategoriesPage";
import { ProductsPage as FutureProductsPage, ProductsPage } from "@src/products/future/ProductsPage";
import { ProductsWithLowPricePage as FutureProductsWithLowPricePage } from "@src/products/future/ProductsWithLowPricePage";
Expand Down
25 changes: 25 additions & 0 deletions demo/admin/src/products/future/ManufacturersPage.cometGen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { type CrudPageConfig } from "@comet/admin-generator";
import { type GQLManufacturer } from "@src/graphql.generated";

export const ManufacturersPage: CrudPageConfig<GQLManufacturer> = {
type: "crudPage",
gqlType: "Manufacturer",
grid: {
import: {
name: "ManufacturersGrid",
import: "./ManufacturersGrid",
},
},
forms: {
import: {
name: "ManufacturerForm",
import: "@src/products/ManufacturerForm", // TODO: Use custom/generated form: "./ManufacturerForm",
},
},
addForm: {
// variant: "dialog", // TODO: Allow rendering a form in a dialog
},
editForm: {
// pageTitle: (row) => `${row.name}`, // TODO: Allow using a row-value for the title, e.g. `row.name`
},
};
26 changes: 0 additions & 26 deletions demo/admin/src/products/future/ManufacturersPage.tsx

This file was deleted.

69 changes: 69 additions & 0 deletions demo/admin/src/products/future/generated/ManufacturersPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// This file has been generated by comet admin-generator.
// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment.

import {
FillSpace,
SaveBoundary,
SaveBoundarySaveButton,
Stack,
StackMainContent,
StackPage,
StackSwitch,
StackToolbar,
ToolbarActions,
ToolbarAutomaticTitleItem,
ToolbarBackButton,
} from "@comet/admin";
import { ManufacturerForm } from "@src/products/ManufacturerForm";
import { useIntl } from "react-intl";

import { ManufacturersGrid } from "./ManufacturersGrid";

const FormToolbar = () => (
<StackToolbar>
<ToolbarBackButton />
<ToolbarAutomaticTitleItem />
<FillSpace />
<ToolbarActions>
<SaveBoundarySaveButton />
</ToolbarActions>
</StackToolbar>
);

export function ManufacturersPage() {
const intl = useIntl();

return (
<Stack topLevelTitle={intl.formatMessage({ id: "manufacturers.topLevelTitle", defaultMessage: "Manufacturers" })}>
<StackSwitch>
<StackPage name="grid">
<StackToolbar>
<ToolbarBackButton />
<ToolbarAutomaticTitleItem />
</StackToolbar>
<StackMainContent fullHeight>
<ManufacturersGrid />
</StackMainContent>
</StackPage>
<StackPage name="add" title={intl.formatMessage({ id: "manufacturers.addPageTitle", defaultMessage: "Add Manufacturer" })}>
<SaveBoundary>
<FormToolbar />
<StackMainContent>
<ManufacturerForm />
</StackMainContent>
</SaveBoundary>
</StackPage>
<StackPage name="edit" title={intl.formatMessage({ id: "manufacturers.editPageTitle", defaultMessage: "Edit Manufacturer" })}>
{(selectedId) => (
<SaveBoundary>
<FormToolbar />
<StackMainContent>
<ManufacturerForm id={selectedId} />
</StackMainContent>
</SaveBoundary>
)}
</StackPage>
</StackSwitch>
</Stack>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { glob } from "glob";
import { introspectionFromSchema } from "graphql";
import { basename, dirname } from "path";

import { generateCrudPage } from "./generateCrudPage/generateCrudPage";
import { generateForm } from "./generateForm/generateForm";
import { type GridCombinationColumnConfig } from "./generateGrid/combinationColumn";
import { generateGrid } from "./generateGrid/generateGrid";
Expand Down Expand Up @@ -173,8 +174,28 @@ export type GridConfig<T extends { __typename?: string }> = {
selectionProps?: "multiSelect" | "singleSelect";
};

type CrudPageGridConfig = {
import: ImportReference;
};

export type CrudPageFormConfig = {
import: ImportReference;
variant?: "fullPage" | "dialog";
pageTitle?: string;
};

export type CrudPageConfig<T extends { __typename?: string }> = {
type: "crudPage";
topLevelTitle?: string;
gqlType: T["__typename"];
grid: CrudPageGridConfig;
forms?: CrudPageFormConfig;
addForm?: Partial<CrudPageFormConfig>;
editForm?: Partial<CrudPageFormConfig>;
};

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type GeneratorConfig = FormConfig<any> | GridConfig<any> | TabsConfig;
export type GeneratorConfig = FormConfig<any> | GridConfig<any> | CrudPageConfig<any> | TabsConfig;

type GQLDocumentConfig = { document: string; export: boolean };
export type GQLDocumentConfigMap = Record<string, GQLDocumentConfig>;
Expand Down Expand Up @@ -210,6 +231,9 @@ async function runGenerate(filePattern = "src/**/*.cometGen.ts") {
generated = generateForm({ exportName, gqlIntrospection, baseOutputFilename, targetDirectory }, config);
} else if (config.type == "grid") {
generated = generateGrid({ exportName, gqlIntrospection, baseOutputFilename, targetDirectory }, config);
} else if (config.type == "crudPage") {
// TODO: Add missing values like above: { exportName, gqlIntrospection, baseOutputFilename, targetDirectory }
generated = generateCrudPage(config);
} else {
throw new Error(`Unknown config type: ${config.type}`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export const formToolbarCode = `
const FormToolbar = () => (
<StackToolbar>
<ToolbarBackButton />
<ToolbarAutomaticTitleItem />
<FillSpace />
<ToolbarActions>
<SaveBoundarySaveButton />
</ToolbarActions>
</StackToolbar>
);
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { camelCase, capitalCase, pascalCase } from "change-case";
import { plural } from "pluralize";

import { type CrudPageConfig, type CrudPageFormConfig, type GeneratorReturn } from "../generate-command";
import { getFormattedMessageString } from "../utils/intl";
import { formToolbarCode } from "./formToolbarCode";
import { generateFormPageCode } from "./generateFormPageCode";
import { generateGridPageCode } from "./generateGridPageCode";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function generateCrudPage(config: CrudPageConfig<any>): GeneratorReturn {
const gqlTypePlural = plural(config.gqlType);
const componentName = pascalCase(`${gqlTypePlural}Page`);
const topLevelTitle = config.topLevelTitle ?? capitalCase(gqlTypePlural);

const rootMessageId = `${camelCase(gqlTypePlural)}`;

const gridConfig = config.grid;

const addFormConfig: Partial<CrudPageFormConfig> = {
...config.forms,
...config.addForm,
};

const editFormConfig: Partial<CrudPageFormConfig> = {
...config.forms,
...config.editForm,
};

const imports: Record<string, string[]> = {
"react-intl": ["useIntl"],
"@comet/admin": [
"FillSpace",
"SaveBoundary",
"SaveBoundarySaveButton",
"Stack",
"StackMainContent",
"StackPage",
"StackSwitch",
"StackToolbar",
"ToolbarActions",
"ToolbarAutomaticTitleItem",
"ToolbarBackButton",
],
"@comet/cms-admin": ["ContentScopeIndicator"],
};

const addImport = (value: string, from: string) => {
if (imports[from]) {
if (!imports[from].includes(value)) {
imports[from].push(value);
}
} else {
imports[from] = [value];
}
};

const hasMultiplePagesToRender = Boolean(addFormConfig.import || editFormConfig.import);

const pagesCodetoRender: string[] = [
generateGridPageCode({ importName: gridConfig.import.name, renderInsideStackPage: hasMultiplePagesToRender }),
];

addImport(gridConfig.import.name, gridConfig.import.import);

if (addFormConfig.import) {
addImport(addFormConfig.import.name, addFormConfig.import.import);
pagesCodetoRender.push(
generateFormPageCode({
importName: addFormConfig.import.name,
type: "add",
titleMessage: getFormattedMessageString(
`${rootMessageId}.addPageTitle`,
addFormConfig.pageTitle ?? `Add ${capitalCase(config.gqlType)}`,
),
}),
);
}

if (editFormConfig.import) {
addImport(editFormConfig.import.name, editFormConfig.import.import);
pagesCodetoRender.push(
generateFormPageCode({
importName: editFormConfig.import.name,
type: "edit",
// TODO: Allow using a row-value for the title, e.g. `row.name`
titleMessage: getFormattedMessageString(
`${rootMessageId}.editPageTitle`,
editFormConfig.pageTitle ?? `Edit ${capitalCase(config.gqlType)}`,
),
}),
);
}

const pagesCodetoRenderString = hasMultiplePagesToRender
? `
<StackSwitch>${pagesCodetoRender.join("")}</StackSwitch>
`
: `<>${pagesCodetoRender.join("")}</>`;

const code = `
${Object.entries(imports)
.map(([key, imports]) => `import { ${imports.join(", ")} } from "${key}";`)
.join("\n")}

${addFormConfig.import || editFormConfig.import ? formToolbarCode : ""}

export function ${componentName}() {
const intl = useIntl();

return (
<Stack topLevelTitle={${getFormattedMessageString(`${rootMessageId}.topLevelTitle`, topLevelTitle)}}>
${pagesCodetoRenderString}
</Stack>
);
}
`;

return {
code,
gqlDocuments: {},
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
type Settings = {
importName: string;
type: "add" | "edit";
titleMessage: string;
};

export const generateFormPageCode = ({ importName, type, titleMessage }: Settings) => {
const pageCode = `
<SaveBoundary>
<FormToolbar />
<StackMainContent>
<${importName} ${type === "edit" ? "id={selectedId}" : ""} />
</StackMainContent>
</SaveBoundary>`;

const editPageCode = `{(selectedId) => (
${pageCode}
)}`;

return `
<StackPage name="${type}" title={${titleMessage}}>
${type === "edit" ? editPageCode : pageCode}
</StackPage>`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
type Settings = {
importName: string;
renderInsideStackPage: boolean;
};

export const generateGridPageCode = ({ importName, renderInsideStackPage }: Settings) => {
const pageCode = `
<StackToolbar>
<ToolbarBackButton />
<ToolbarAutomaticTitleItem />
</StackToolbar>
<StackMainContent fullHeight>
<${importName} />
</StackMainContent>
`;

if (renderInsideStackPage) {
return `
<StackPage name="grid">
${pageCode}
</StackPage>`;
}

return pageCode;
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
export const getFormattedMessageNode = (id: string, defaultMessage = "", values?: string) => {
return `<FormattedMessage id="${id}" defaultMessage={\`${defaultMessage}\`} ${values ? `values={${values}}` : ""} />`;
};

export const getFormattedMessageString = (id: string, defaultMessage = "") => {
return `intl.formatMessage({ id: "${id}", defaultMessage: "${defaultMessage}" })`;
};
9 changes: 8 additions & 1 deletion packages/admin/admin-generator/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
export type { FormConfig, FormFieldConfig, GeneratorConfig, GridColumnConfig, GridConfig } from "./commands/generate/generate-command";
export type {
CrudPageConfig,
FormConfig,
FormFieldConfig,
GeneratorConfig,
GridColumnConfig,
GridConfig,
} from "./commands/generate/generate-command";