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 (Future): Add support for startAdornments and endAdornments for form fields #2636

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 10 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
7 changes: 7 additions & 0 deletions .changeset/shaggy-waves-fix.md
Copy link
Collaborator

Choose a reason for hiding this comment

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

Thanks for the changeset! However, we decided that we don't add changesets for the Admin Generator. So please remove/adapt it.

jamesricky marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@comet/admin-date-time": minor
---

Change `endAdornment` in `DatePicker` from `boolean` to `React.ReactNode`
jamesricky marked this conversation as resolved.
Show resolved Hide resolved

Instead of enabling a fixed icon, a custom `endAdornment` can be given to `DatePicker`.
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const CreateCapProductForm: FormConfig<GQLProduct> = {
{ type: "text", name: "description", label: "Description", multiline: true },
{ type: "asyncSelect", name: "category", rootQuery: "productCategories" },
{ type: "boolean", name: "inStock" },
{ type: "date", name: "availableSince" },
{ type: "date", name: "availableSince", startAdornment: { icon: "CalendarToday" } },
{ type: "block", name: "image", label: "Image", block: { name: "DamImageBlock", import: "@comet/cms-admin" } },
],
};
11 changes: 9 additions & 2 deletions demo/admin/src/products/future/ProductForm.cometGen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,14 @@ export const ProductForm: FormConfig<GQLProduct> = {
values: [{ value: "Cap", label: "great Cap" }, "Shirt", "Tie"],
},
{ type: "asyncSelect", name: "category", rootQuery: "productCategories" },
{ type: "numberRange", name: "priceRange", minValue: 25, maxValue: 500, disableSlider: true, startAdornment: "€" },
{
type: "numberRange",
name: "priceRange",
minValue: 25,
maxValue: 500,
disableSlider: true,
startAdornment: "€",
},
{
type: "optionalNestedFields",
name: "dimensions",
Expand All @@ -59,7 +66,7 @@ export const ProductForm: FormConfig<GQLProduct> = {
},
},
{ type: "boolean", name: "inStock" },
{ type: "date", name: "availableSince" },
{ type: "date", name: "availableSince", startAdornment: { icon: "CalendarToday" } },
{ 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
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ export const ProductPriceForm: FormConfig<GQLProduct> = {
gqlType: "Product",
mode: "edit",
fragmentName: "ProductPriceFormDetails", // configurable as it must be unique across project
fields: [{ type: "number", name: "price", helperText: "Enter price in this format: 123,45" }],
fields: [{ type: "number", name: "price", helperText: "Enter price in this format: 123,45", startAdornment: "€" }],
};
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import {
useStackSwitchApi,
} from "@comet/admin";
import { FinalFormDatePicker } from "@comet/admin-date-time";
import { CalendarToday as CalendarTodayIcon } from "@comet/admin-icons";
import { BlockState, createFinalFormBlock } from "@comet/blocks-admin";
import { DamImageBlock } from "@comet/cms-admin";
import { FormControlLabel } from "@mui/material";
import { FormControlLabel, InputAdornment } from "@mui/material";
import { GQLProductType } from "@src/graphql.generated";
import { FormApi } from "final-form";
import isEqual from "lodash.isequal";
Expand Down Expand Up @@ -147,6 +148,11 @@ export function CreateCapProductForm({ type }: FormProps): React.ReactElement {
name="availableSince"
component={FinalFormDatePicker}
label={<FormattedMessage id="product.availableSince" defaultMessage="Available Since" />}
startAdornment={
<InputAdornment position="start">
<CalendarTodayIcon />
</InputAdornment>
}
/>
<Field
name="image"
Expand Down
7 changes: 6 additions & 1 deletion demo/admin/src/products/future/generated/ProductForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
useStackSwitchApi,
} from "@comet/admin";
import { FinalFormDatePicker } from "@comet/admin-date-time";
import { Lock } from "@comet/admin-icons";
import { CalendarToday as CalendarTodayIcon, Lock } from "@comet/admin-icons";
import { BlockState, createFinalFormBlock } from "@comet/blocks-admin";
import {
DamImageBlock,
Expand Down Expand Up @@ -396,6 +396,11 @@ export function ProductForm({ id }: FormProps): React.ReactElement {
name="availableSince"
component={FinalFormDatePicker}
label={<FormattedMessage id="product.availableSince" defaultMessage="Available Since" />}
startAdornment={
<InputAdornment position="start">
<CalendarTodayIcon />
</InputAdornment>
}
/>
<Field
name="image"
Expand Down
2 changes: 2 additions & 0 deletions demo/admin/src/products/future/generated/ProductPriceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { useApolloClient, useQuery } from "@apollo/client";
import { Field, filterByFragment, FinalForm, FinalFormInput, Loading, useFormApiRef } from "@comet/admin";
import { queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin";
import { InputAdornment } from "@mui/material";
import { FormApi } from "final-form";
import isEqual from "lodash.isequal";
import React from "react";
Expand Down Expand Up @@ -94,6 +95,7 @@ export function ProductPriceForm({ id }: FormProps): React.ReactElement {
component={FinalFormInput}
type="number"
label={<FormattedMessage id="product.price" defaultMessage="Price" />}
startAdornment={<InputAdornment position="start">€</InputAdornment>}
helperText={<FormattedMessage id="product.price.helperText" defaultMessage="Enter price in this format: 123,45" />}
/>
</>
Expand Down
13 changes: 7 additions & 6 deletions packages/admin/admin-date-time/src/datePicker/DatePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import "react-date-range/dist/theme/default.css";

import { ClearInputAdornment, InputWithPopperProps } from "@comet/admin";
import { Calendar as CalendarIcon } from "@comet/admin-icons";
import { ComponentsOverrides } from "@mui/material";
import { ComponentsOverrides, InputAdornment } from "@mui/material";
import { Theme, useThemeProps } from "@mui/material/styles";
import { FormatDateOptions, useIntl } from "react-intl";

Expand All @@ -28,13 +28,14 @@ export const DatePicker = (inProps: DatePickerProps) => {
onChange,
value,
formatDateOptions,
endAdornment,
required,
placeholder,
monthsToShow,
minDate = defaultMinDate,
maxDate = defaultMaxDate,
slotProps,
startAdornment,
jamesricky marked this conversation as resolved.
Show resolved Hide resolved
endAdornment,
...inputWithPopperProps
} = useThemeProps({ props: inProps, name: "CometAdminDatePicker" });
const intl = useIntl();
Expand All @@ -46,7 +47,7 @@ export const DatePicker = (inProps: DatePickerProps) => {
value={value ? intl.formatDate(value, formatDateOptions) : ""}
startAdornment={
<StartAdornment position="start" disablePointerEvents {...slotProps?.startAdornment}>
<CalendarIcon />
{startAdornment ? startAdornment : <CalendarIcon />}
</StartAdornment>
}
jamesricky marked this conversation as resolved.
Show resolved Hide resolved
placeholder={placeholder ?? intl.formatMessage({ id: "comet.datePicker.selectDate", defaultMessage: "Select date" })}
Expand All @@ -56,12 +57,12 @@ export const DatePicker = (inProps: DatePickerProps) => {
required={required}
endAdornment={
!required ? (
<>
<InputAdornment position="end">
johnnyomair marked this conversation as resolved.
Show resolved Hide resolved
jamesricky marked this conversation as resolved.
Show resolved Hide resolved
<ClearInputAdornment position="end" hasClearableContent={Boolean(value)} onClick={() => onChange && onChange(undefined)} />
{endAdornment}
</>
</InputAdornment>
) : (
endAdornment
<InputAdornment position="end">{endAdornment}</InputAdornment>
jamesricky marked this conversation as resolved.
Show resolved Hide resolved
)
}
>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,55 @@
import { IntrospectionEnumType, IntrospectionInputValue, IntrospectionNamedTypeRef, IntrospectionObjectType, IntrospectionQuery } from "graphql";

import { FormConfig, FormFieldConfig, isFormFieldConfig } from "../generator";
import { Adornment, FormConfig, FormFieldConfig, isFormFieldConfig } from "../generator";
import { camelCaseToHumanReadable } from "../utils/camelCaseToHumanReadable";
import { findQueryTypeOrThrow } from "../utils/findQueryType";
import { Imports } from "../utils/generateImportsCode";
import { isFieldOptional } from "../utils/isFieldOptional";
import { findFieldByName, GenerateFieldsReturn } from "./generateFields";

type AdornmentData = {
adornmentString: string;
adornmentImport?: { name: string; importPath: string };
};

const getAdornmentData = ({ adornmentData }: { adornmentData: Adornment }): AdornmentData => {
let adornmentString = "";
let adornmentImport = { name: "", importPath: "" };

if (typeof adornmentData === "string") {
return { adornmentString: adornmentData };
}

if (typeof adornmentData.icon === "string") {
adornmentString = `<${adornmentData.icon}Icon />`;
adornmentImport = {
name: `${adornmentData.icon} as ${adornmentData.icon}Icon`,
importPath: "@comet/admin-icons",
};
} else if (typeof adornmentData.icon === "object") {
if ("import" in adornmentData.icon) {
adornmentString = `<${adornmentData.icon.name} />`;
adornmentImport = {
name: `${adornmentData.icon.name} as ${adornmentData.icon.name}Icon`,
importPath: "@comet/admin-icons",
};
jamesricky marked this conversation as resolved.
Show resolved Hide resolved
} else {
const { name, ...iconProps } = adornmentData.icon;
adornmentString = `<${name}Icon
${Object.entries(iconProps)
.map(([key, value]) => `${key}="${value}"`)
.join("\n")}
/>`;
adornmentImport = {
name: `${adornmentData.icon.name} as ${adornmentData.icon.name}Icon`,
importPath: "@comet/admin-icons",
};
}
}
johnnyomair marked this conversation as resolved.
Show resolved Hide resolved

return { adornmentString, adornmentImport };
};

function getTypeInfo(arg: IntrospectionInputValue, gqlIntrospection: IntrospectionQuery) {
let typeKind = undefined;
let typeClass = "unknown";
Expand Down Expand Up @@ -102,6 +145,27 @@ export function generateFormField({

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

let startAdornment: AdornmentData = { adornmentString: "" };
let endAdornment: AdornmentData = { adornmentString: "" };

if (config.startAdornment) {
startAdornment = getAdornmentData({
adornmentData: config.startAdornment,
});
if (startAdornment.adornmentImport) {
imports.push(startAdornment.adornmentImport);
}
}

if (config.endAdornment) {
endAdornment = getAdornmentData({
adornmentData: config.endAdornment,
});
if (endAdornment.adornmentImport) {
imports.push(endAdornment.adornmentImport);
}
}

let code = "";
let formValueToGqlInputCode = "";
let formFragmentField = name;
Expand All @@ -115,6 +179,8 @@ export function generateFormField({
fullWidth
name="${nameWithPrefix}"
label={${fieldLabel}}
${config.startAdornment ? `startAdornment={<InputAdornment position="start">${startAdornment.adornmentString}</InputAdornment>}` : ""}
nsams marked this conversation as resolved.
Show resolved Hide resolved
${config.endAdornment ? `endAdornment={<InputAdornment position="end">${endAdornment.adornmentString}</InputAdornment>}` : ""}
${
config.helperText
? `helperText={<FormattedMessage id=` +
Expand All @@ -135,6 +201,8 @@ export function generateFormField({
component={FinalFormInput}
type="number"
label={${fieldLabel}}
${config.startAdornment ? `startAdornment={<InputAdornment position="start">${startAdornment.adornmentString}</InputAdornment>}` : ""}
${config.endAdornment ? `endAdornment={<InputAdornment position="end">${endAdornment.adornmentString}</InputAdornment>}` : ""}
${
config.helperText
? `helperText={<FormattedMessage id=` +
Expand Down Expand Up @@ -178,8 +246,8 @@ export function generateFormField({
min={${config.minValue}}
max={${config.maxValue}}
${config.disableSlider ? "disableSlider" : ""}
${config.startAdornment ? `startAdornment={<InputAdornment position="start">${config.startAdornment}</InputAdornment>}` : ""}
${config.endAdornment ? `endAdornment={<InputAdornment position="end">${config.endAdornment}</InputAdornment>}` : ""}
${config.startAdornment ? `startAdornment={<InputAdornment position="start">${startAdornment.adornmentString}</InputAdornment>}` : ""}
${config.endAdornment ? `endAdornment={<InputAdornment position="end">${endAdornment.adornmentString}</InputAdornment>}` : ""}
${
config.helperText
? `helperText={<FormattedMessage id=` +
Expand Down Expand Up @@ -225,6 +293,8 @@ export function generateFormField({
name="${nameWithPrefix}"
component={FinalFormDatePicker}
label={${fieldLabel}}
${config.startAdornment ? `startAdornment={<InputAdornment position="start">${startAdornment.adornmentString}</InputAdornment>}` : ""}
${config.endAdornment ? `endAdornment={<InputAdornment position="end">${endAdornment.adornmentString}</InputAdornment>}` : ""}
${
config.helperText
? `helperText={<FormattedMessage id=` +
Expand Down
35 changes: 25 additions & 10 deletions packages/admin/cms-admin/src/generator/future/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ type ImportReference = {
import: string;
};

type IconObject = Pick<IconProps, "color" | "fontSize"> & {
name: IconName;
};

type Icon = IconName | IconObject | ImportReference;
export type Adornment = string | { icon: Icon };

type SingleFileFormFieldConfig = { type: "fileUpload"; multiple?: false; maxFiles?: 1 } & Pick<
Partial<FinalFormFileUploadProps<false>>,
"maxFileSize" | "readOnly" | "layout" | "accept"
Expand All @@ -40,13 +47,15 @@ export type FormFieldConfig<T> = (
minValue: number;
maxValue: number;
disableSlider?: boolean;
startAdornment?: string;
endAdornment?: string;
}
| { type: "boolean" }
| { type: "date" }
// TODO | { type: "dateTime" }
| { type: "staticSelect"; values?: Array<{ value: string; label: string } | string>; inputType?: "select" | "radio" }
// TODO | { type: "dateTime"; }
| {
type: "staticSelect";
values?: Array<{ value: string; label: string } | string>;
inputType?: "select" | "radio";
}
| {
type: "asyncSelect";
rootQuery: string;
Expand All @@ -56,7 +65,17 @@ export type FormFieldConfig<T> = (
| { type: "block"; block: ImportReference }
| SingleFileFormFieldConfig
| MultiFileFormFieldConfig
) & { name: keyof T; label?: string; required?: boolean; virtual?: boolean; validate?: ImportReference; helperText?: string; readOnly?: boolean };
) & {
name: keyof T;
label?: string;
required?: boolean;
virtual?: boolean;
validate?: ImportReference;
helperText?: string;
readOnly?: boolean;
startAdornment?: Adornment;
endAdornment?: Adornment;
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isFormFieldConfig<T>(arg: any): arg is FormFieldConfig<T> {
return !isFormLayoutConfig(arg);
Expand Down Expand Up @@ -101,14 +120,10 @@ export type BaseColumnConfig = Pick<GridColDef, "headerName" | "width" | "minWid
visible?: ColumnVisibleOption;
};

type IconObject = Pick<IconProps, "color" | "fontSize"> & {
name: IconName;
};

export type StaticSelectLabelCellContent = {
primaryText?: string;
secondaryText?: string;
icon?: IconName | IconObject | ImportReference;
icon?: Icon;
};

export type GridColumnConfig<T> = (
Expand Down
Loading