Skip to content

Commit

Permalink
Admin Generator (Future): Add support for file upload fields in forms (
Browse files Browse the repository at this point in the history
  • Loading branch information
jamesricky authored Aug 21, 2024
1 parent f3d9cec commit 05e154a
Show file tree
Hide file tree
Showing 6 changed files with 110 additions and 13 deletions.
2 changes: 2 additions & 0 deletions demo/admin/src/products/future/ProductForm.cometGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ export const ProductForm: FormConfig<GQLProduct> = {
{ type: "boolean", name: "inStock" },
{ type: "date", name: "availableSince" },
{ type: "block", name: "image", label: "Image", block: { name: "DamImageBlock", import: "@comet/cms-admin" } },
{ type: "fileUpload", name: "priceList", label: "Price List", maxFileSize: 1024 * 1024 * 4 },
{ type: "fileUpload", name: "datasheets", label: "Datasheets", multiple: true, maxFileSize: 1024 * 1024 * 4 },
],
},
],
Expand Down
8 changes: 8 additions & 0 deletions demo/admin/src/products/future/generated/ProductForm.gql.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// 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 { gql } from "@apollo/client";
import { finalFormFileUploadFragment } from "@comet/cms-admin";

export const productFormFragment = gql`
fragment ProductFormDetails on Product {
Expand All @@ -16,7 +17,14 @@ export const productFormFragment = gql`
inStock
availableSince
image
priceList {
...FinalFormFileUpload
}
datasheets {
...FinalFormFileUpload
}
}
${finalFormFileUploadFragment}
`;
export const productQuery = gql`
query Product($id: ID!) {
Expand Down
31 changes: 28 additions & 3 deletions demo/admin/src/products/future/generated/ProductForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ import {
import { FinalFormDatePicker } from "@comet/admin-date-time";
import { Lock } from "@comet/admin-icons";
import { BlockState, createFinalFormBlock } from "@comet/blocks-admin";
import { DamImageBlock, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin";
import {
DamImageBlock,
FileUploadField,
GQLFinalFormFileUploadFragment,
queryUpdatedAt,
resolveHasSaveConflict,
useFormSaveConflict,
} from "@comet/cms-admin";
import { FormControlLabel, InputAdornment, MenuItem } from "@mui/material";
import { FormApi } from "final-form";
import isEqual from "lodash.isequal";
Expand All @@ -45,7 +52,12 @@ const rootBlocks = {
image: DamImageBlock,
};

type FormValues = GQLProductFormDetailsFragment & {
type ProductFormDetailsFragment = Omit<GQLProductFormDetailsFragment, "priceList" | "datasheets"> & {
priceList: GQLFinalFormFileUploadFragment | null;
datasheets: GQLFinalFormFileUploadFragment[];
};

type FormValues = ProductFormDetailsFragment & {
image: BlockState<typeof rootBlocks.image>;
};

Expand All @@ -68,7 +80,7 @@ export function ProductForm({ id }: FormProps): React.ReactElement {
() =>
data?.product
? {
...filterByFragment<GQLProductFormDetailsFragment>(productFormFragment, data.product),
...filterByFragment<ProductFormDetailsFragment>(productFormFragment, data.product),
createdAt: data.product.createdAt ? new Date(data.product.createdAt) : undefined,
availableSince: data.product.availableSince ? new Date(data.product.availableSince) : undefined,
image: rootBlocks.image.input2State(data.product.image),
Expand Down Expand Up @@ -97,6 +109,8 @@ export function ProductForm({ id }: FormProps): React.ReactElement {
...formValues,
category: formValues.category?.id,
image: rootBlocks.image.state2Output(formValues.image),
priceList: formValues.priceList ? formValues.priceList.id : null,
datasheets: formValues.datasheets?.map(({ id }) => id),
};
if (mode === "edit") {
if (!id) throw new Error();
Expand Down Expand Up @@ -261,6 +275,17 @@ export function ProductForm({ id }: FormProps): React.ReactElement {
<Field name="image" isEqual={isEqual}>
{createFinalFormBlock(rootBlocks.image)}
</Field>
<FileUploadField
name="priceList"
label={<FormattedMessage id="product.priceList" defaultMessage="Price List" />}
maxFileSize={4194304}
/>
<FileUploadField
name="datasheets"
label={<FormattedMessage id="product.datasheets" defaultMessage="Datasheets" />}
multiple
maxFileSize={4194304}
/>
</FieldSet>
</MainContent>
</>
Expand Down
36 changes: 33 additions & 3 deletions packages/admin/cms-admin/src/generator/future/generateForm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export function generateForm(
});

const readOnlyFields = formFields.filter((field) => field.readOnly);
const fileFields = formFields.filter((field) => field.type == "fileUpload");

let hooksCode = "";
let formValueToGqlInputCode = "";
Expand All @@ -115,6 +116,7 @@ export function generateForm(
fragment ${fragmentName} on ${gqlType} {
${formFragmentFields.join("\n")}
}
${fileFields.length > 0 ? "${finalFormFileUploadFragment}" : ""}
`;

if (editMode) {
Expand Down Expand Up @@ -194,6 +196,31 @@ export function generateForm(
});
}

let filterByFragmentType = `GQL${fragmentName}Fragment`;
let customFilterByFragment = "";

if (fileFields.length > 0) {
const keysToOverride = fileFields.map((field) => field.name);

customFilterByFragment = `type ${fragmentName}Fragment = Omit<${filterByFragmentType}, ${keysToOverride
.map((key) => `"${String(key)}"`)
.join(" | ")}> & {
${fileFields
.map((field) => {
if (
("multiple" in field && field.multiple) ||
("maxFiles" in field && typeof field.maxFiles === "number" && field.maxFiles > 1)
) {
return `${String(field.name)}: GQLFinalFormFileUploadFragment[];`;
}
return `${String(field.name)}: GQLFinalFormFileUploadFragment | null;`;
})
.join("\n")}
}`;

filterByFragmentType = `${fragmentName}Fragment`;
}

const code = `import { useApolloClient, useQuery, gql } from "@apollo/client";
import {
AsyncSelectField,
Expand All @@ -214,6 +241,7 @@ export function generateForm(
import { ArrowLeft, Lock } from "@comet/admin-icons";
import { FinalFormDatePicker } from "@comet/admin-date-time";
import { BlockState, createFinalFormBlock } from "@comet/blocks-admin";
import { queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict, FileUploadField, GQLFinalFormFileUploadFragment } from "@comet/cms-admin";
import { queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin";
import { FormControlLabel, IconButton, MenuItem, InputAdornment } from "@mui/material";
import { FormApi } from "final-form";
Expand All @@ -229,13 +257,15 @@ export function generateForm(
: ""
}
${customFilterByFragment}
type FormValues = ${
formValuesConfig.filter((config) => !!config.omitFromFragmentType).length > 0
? `Omit<GQL${fragmentName}Fragment, ${formValuesConfig
? `Omit<${filterByFragmentType}, ${formValuesConfig
.filter((config) => !!config.omitFromFragmentType)
.map((config) => `"${config.omitFromFragmentType}"`)
.join(" | ")}>`
: `GQL${fragmentName}Fragment`
: `${filterByFragmentType}`
} ${
formValuesConfig.filter((config) => !!config.typeCode).length > 0
? `& {
Expand Down Expand Up @@ -270,7 +300,7 @@ export function generateForm(
editMode
? `const initialValues = React.useMemo<Partial<FormValues>>(() => data?.${instanceGqlType}
? {
...filterByFragment<GQL${fragmentName}Fragment>(${instanceGqlType}FormFragment, data.${instanceGqlType}),
...filterByFragment<${filterByFragmentType}>(${instanceGqlType}FormFragment, data.${instanceGqlType}),
${formValuesConfig
.filter((config) => !!config.initializationCode)
.map((config) => config.initializationCode)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ export function generateFormField({
validateCode = `validate={${config.validate.name}}`;
}

const fieldLabel = `<FormattedMessage id="${instanceGqlType}.${name}" defaultMessage="${label}" />`;

let code = "";
let formValueToGqlInputCode = "";
let formFragmentField = name;
Expand All @@ -77,7 +79,7 @@ export function generateFormField({
variant="horizontal"
fullWidth
name="${name}"
label={<FormattedMessage id="${instanceGqlType}.${name}" defaultMessage="${label}" />}
label={${fieldLabel}}
${
config.helperText
? `helperText={<FormattedMessage id=` + `"${instanceGqlType}.${name}.helperText" ` + `defaultMessage="${config.helperText}" />}`
Expand All @@ -95,7 +97,7 @@ export function generateFormField({
name="${name}"
component={FinalFormInput}
type="number"
label={<FormattedMessage id="${instanceGqlType}.${name}" defaultMessage="${label}" />}
label={${fieldLabel}}
${
config.helperText
? `helperText={<FormattedMessage id=` +
Expand Down Expand Up @@ -130,7 +132,7 @@ export function generateFormField({
code = `<Field name="${name}" label="" type="checkbox" variant="horizontal" fullWidth ${validateCode}>
{(props) => (
<FormControlLabel
label={<FormattedMessage id="${instanceGqlType}.${name}" defaultMessage="${label}" />}
label={${fieldLabel}}
control={<FinalFormCheckbox ${config.readOnly ? readOnlyProps : ""} {...props} />}
${
config.helperText
Expand Down Expand Up @@ -159,7 +161,7 @@ export function generateFormField({
fullWidth
name="${name}"
component={FinalFormDatePicker}
label={<FormattedMessage id="${instanceGqlType}.${name}" defaultMessage="${label}" />}
label={${fieldLabel}}
${
config.helperText
? `helperText={<FormattedMessage id=` +
Expand Down Expand Up @@ -192,6 +194,22 @@ export function generateFormField({
},
},
];
} else if (config.type === "fileUpload") {
const multiple = config.multiple || (typeof config.maxFiles === "number" && config.maxFiles > 1);
code = `<FileUploadField name="${name}" label={${fieldLabel}}
${config.multiple ? "multiple" : ""}
${config.maxFiles ? `maxFiles={${config.maxFiles}}` : ""}
${config.maxFileSize ? `maxFileSize={${config.maxFileSize}}` : ""}
${config.readOnly ? `readOnly` : ""}
${config.layout ? `layout="${config.layout}"` : ""}
${config.accept ? `accept="${config.accept}"` : ""}
/>`;
if (multiple) {
formValueToGqlInputCode = `${name}: formValues.${name}?.map(({ id }) => id),`;
} else {
formValueToGqlInputCode = `${name}: formValues.${name} ? formValues.${name}.id : null,`;
}
formFragmentField = `${name} { ...FinalFormFileUpload }`;
} else if (config.type == "staticSelect") {
const enumType = gqlIntrospection.__schema.types.find(
(t) => t.kind === "ENUM" && t.name === (introspectionFieldType as IntrospectionNamedTypeRef).name,
Expand All @@ -212,7 +230,7 @@ export function generateFormField({
variant="horizontal"
fullWidth
name="${name}"
label={<FormattedMessage id="${instanceGqlType}.${name}" defaultMessage="${label}" />}>
label={${fieldLabel}}>
${
config.helperText
? `helperText={<FormattedMessage id=` + `"${instanceGqlType}.${name}.helperText" ` + `defaultMessage="${config.helperText}" />}`
Expand Down Expand Up @@ -282,7 +300,7 @@ export function generateFormField({
variant="horizontal"
fullWidth
name="${name}"
label={<FormattedMessage id="${instanceGqlType}.${name}" defaultMessage="${label}" />}
label={${fieldLabel}}
loadOptions={async () => {
const { data } = await client.query<GQL${queryName}Query, GQL${queryName}QueryVariables>({
query: gql\`query ${queryName} {
Expand Down
16 changes: 15 additions & 1 deletion packages/admin/cms-admin/src/generator/future/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { glob } from "glob";
import { introspectionFromSchema } from "graphql";
import { basename, dirname } from "path";

import { FinalFormFileUploadProps } from "../../form/file/FinalFormFileUpload";
import { generateForm } from "./generateForm";
import { generateGrid } from "./generateGrid";
import { UsableFields } from "./generateGrid/usableFields";
Expand All @@ -15,6 +16,16 @@ type ImportReference = {
import: string;
};

type SingleFileFormFieldConfig = { type: "fileUpload"; multiple?: false; maxFiles?: 1 } & Pick<
Partial<FinalFormFileUploadProps<false>>,
"maxFileSize" | "readOnly" | "layout" | "accept"
>;

type MultiFileFormFieldConfig = { type: "fileUpload"; multiple: true; maxFiles?: number } & Pick<
Partial<FinalFormFileUploadProps<true>>,
"maxFileSize" | "readOnly" | "layout" | "accept"
>;

export type FormFieldConfig<T> = (
| { type: "text"; multiline?: boolean }
| { type: "number" }
Expand All @@ -24,6 +35,8 @@ export type FormFieldConfig<T> = (
| { type: "staticSelect"; values?: Array<{ value: string; label: string } | string> }
| { type: "asyncSelect"; rootQuery: string; labelField?: string }
| { type: "block"; block: ImportReference }
| SingleFileFormFieldConfig
| MultiFileFormFieldConfig
) & { name: keyof T; label?: string; required?: boolean; virtual?: boolean; validate?: ImportReference; helperText?: string; readOnly?: boolean };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isFormFieldConfig<T>(arg: any): arg is FormFieldConfig<T> {
Expand Down Expand Up @@ -127,8 +140,9 @@ export async function runFutureGenerate(filePattern = "src/**/*.cometGen.ts") {
if (gqlDocumentsOutputCode != "") {
const gqlDocumentsOuputFilename = `${targetDirectory}/${basename(file.replace(/\.cometGen\.ts$/, ""))}.gql.tsx`;
gqlDocumentsOutputCode = `import { gql } from "@apollo/client";
import { finalFormFileUploadFragment } from "@comet/cms-admin";
${gqlDocumentsOutputCode}
${gqlDocumentsOutputCode}
`;
await writeGenerated(gqlDocumentsOuputFilename, gqlDocumentsOutputCode);
}
Expand Down

0 comments on commit 05e154a

Please sign in to comment.