From 9ca9cab0fd1cd9b28a4523709c59e97923f827c7 Mon Sep 17 00:00:00 2001 From: Colin Regourd Date: Fri, 13 Sep 2024 18:31:06 +0200 Subject: [PATCH 1/5] Start Embedded form --- apps/example/options.tsx | 3 + packages/next-admin/src/appHandler.ts | 73 ++++++++++- packages/next-admin/src/components/Form.tsx | 78 +++++++----- .../next-admin/src/components/FormHeader.tsx | 4 +- .../next-admin/src/components/NextAdmin.tsx | 4 +- .../src/components/inputs/ArrayField.tsx | 22 ++-- .../src/components/inputs/DndItem.tsx | 46 +++++-- .../inputs/EmbeddedForm/EmbeddedFormModal.tsx | 118 ++++++++++++++++++ .../src/context/CustomInputsContext.tsx | 22 ++++ packages/next-admin/src/types.ts | 12 +- packages/next-admin/src/utils/jsonSchema.ts | 8 +- packages/next-admin/src/utils/prisma.ts | 1 - packages/next-admin/src/utils/props.ts | 28 ++--- packages/next-admin/src/utils/server.ts | 59 ++++----- packages/next-admin/src/utils/tools.ts | 3 + 15 files changed, 373 insertions(+), 108 deletions(-) create mode 100644 packages/next-admin/src/components/inputs/EmbeddedForm/EmbeddedFormModal.tsx create mode 100644 packages/next-admin/src/context/CustomInputsContext.tsx diff --git a/apps/example/options.tsx b/apps/example/options.tsx index 1c1080a1..8ab18859 100644 --- a/apps/example/options.tsx +++ b/apps/example/options.tsx @@ -107,6 +107,9 @@ export const options: NextAdminOptions = { display: "list", orderField: "order", }, + role: { + disabled: true, + }, avatar: { format: "file", handler: { diff --git a/packages/next-admin/src/appHandler.ts b/packages/next-admin/src/appHandler.ts index cbdc3821..93a94cce 100644 --- a/packages/next-admin/src/appHandler.ts +++ b/packages/next-admin/src/appHandler.ts @@ -1,16 +1,26 @@ +import { cloneDeep } from "lodash"; import { createEdgeRouter } from "next-connect"; import { NextRequest, NextResponse } from "next/server"; +import { HookError } from "./exceptions/HookError"; import { handleOptionsSearch } from "./handlers/options"; import { deleteResource, submitResource } from "./handlers/resources"; -import { CreateAppHandlerParams, Permission, RequestContext } from "./types"; +import { + CreateAppHandlerParams, + EditFieldsOptions, + Permission, + RequestContext, +} from "./types"; +import { getSchemaForResource, getSchemas } from "./utils/jsonSchema"; import { hasPermission } from "./utils/permissions"; +import { getDataItem } from "./utils/prisma"; import { formatId, getFormValuesFromFormData, + getPrismaModelForResource, getResourceFromParams, getResources, + transformSchema, } from "./utils/server"; -import { HookError } from "./exceptions/HookError"; export const createHandler =

({ apiBasePath, @@ -36,6 +46,65 @@ export const createHandler =

({ } router + .get(`${apiBasePath}/:model/:id?`, async (req, ctx) => { + const resource = getResourceFromParams(ctx.params[paramKey], resources); + + if (!resource) { + return NextResponse.json( + { error: "Resource not found" }, + { status: 404 } + ); + } + + const id = + ctx.params[paramKey].length === 2 + ? formatId(resource, ctx.params[paramKey].at(-1)!) + : undefined; + + if (id && hasPermission(options?.model?.[resource], Permission.EDIT)) { + const data = await getDataItem({ + prisma, + resource, + resourceId: id, + options, + }); + + let deepCopySchema = await transformSchema( + resource, + options, + data + )(cloneDeep(schema)); + + const dmmfSchema = getPrismaModelForResource(resource); + + const editFieldOptions = options?.model?.[resource]?.edit + ?.fields as EditFieldsOptions; + + const modelSchema = + resource && schema + ? getSchemaForResource(deepCopySchema, resource) + : undefined; + + const { uiSchema } = getSchemas( + data, + modelSchema, + dmmfSchema?.fields ?? [], + editFieldOptions + ); + + return NextResponse.json({ data, schema: modelSchema, uiSchema }); + } else if ( + !id && + hasPermission(options?.model?.[resource], Permission.CREATE) + ) { + return NextResponse.json({ data: {} }); + } else { + return NextResponse.json( + { error: "You don't have permission to view this resource" }, + { status: 403 } + ); + } + }) .post(`${apiBasePath}/:model/actions/:id`, async (req, ctx) => { const id = ctx.params[paramKey].at(-1)!; diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx index 00c310e8..d1dddf07 100644 --- a/packages/next-admin/src/components/Form.tsx +++ b/packages/next-admin/src/components/Form.tsx @@ -29,6 +29,9 @@ import React, { } from "react"; import { twMerge } from "tailwind-merge"; import { useConfig } from "../context/ConfigContext"; +import CustomInputsProvider, { + useCustomInputs, +} from "../context/CustomInputsContext"; import FormDataProvider, { useFormData } from "../context/FormDataContext"; import FormStateProvider, { useFormState } from "../context/FormStateContext"; import { useI18n } from "../context/I18nContext"; @@ -39,6 +42,7 @@ import { EditFieldsOptions, Field, FormProps, + FormWrapperProps, ModelName, ModelOptions, Permission, @@ -47,6 +51,7 @@ import { getSchemas } from "../utils/jsonSchema"; import { formatLabel, slugify } from "../utils/tools"; import FormHeader from "./FormHeader"; import ArrayField from "./inputs/ArrayField"; +import BaseInput from "./inputs/BaseInput"; import CheckboxWidget from "./inputs/CheckboxWidget"; import DateTimeWidget from "./inputs/DateTimeWidget"; import DateWidget from "./inputs/DateWidget"; @@ -64,7 +69,6 @@ import { TooltipRoot, TooltipTrigger, } from "./radix/Tooltip"; -import BaseInput from "./inputs/BaseInput"; const RichTextField = dynamic(() => import("./inputs/RichText/RichTextField"), { ssr: false, @@ -84,13 +88,13 @@ const widgets: RjsfForm["props"]["widgets"] = { TextareaWidget: TextareaWidget, }; -const Form = ({ +export const Form = ({ data, schema, - dmmfSchema, + uiSchema, resource, + id, validation: validationProp, - customInputs, }: FormProps) => { const [validation, setValidation] = useState(validationProp); const { basePath, options, apiBasePath } = useConfig(); @@ -105,13 +109,8 @@ const Form = ({ const canCreate = !modelOptions?.permissions || modelOptions?.permissions?.includes(Permission.CREATE); - const { edit, id, ...schemas } = getSchemas( - data, - schema, - dmmfSchema, - modelOptions?.edit?.fields as EditFieldsOptions - ); const { router } = useRouterInternal(); + const edit = !!data; const { t } = useI18n(); const formRef = useRef(null); const [isPending, setIsPending] = useState(false); @@ -120,6 +119,7 @@ const Form = ({ const { showMessage } = useMessage(); const { cleanAll } = useFormState(); const { setFormData } = useFormData(); + const customInputs = useCustomInputs(); useEffect(() => { if (!edit && !canCreate) { @@ -539,7 +539,8 @@ const Form = ({ tagName={CustomForm} idPrefix="" idSeparator="" - {...schemas} + schema={schema} + uiSchema={uiSchema} onChange={(e) => { setFormData(e.formData); }} @@ -548,7 +549,7 @@ const Form = ({ extraErrors={extraErrors} fields={fields} disabled={allDisabled} - formContext={{ isPending, dmmfSchema }} + formContext={{ isPending }} templates={{ ...templates, ButtonTemplates: { SubmitButton: submitButton }, @@ -562,31 +563,52 @@ const Form = ({ [submitButton] ); - return ( -

-
- -
- -
-
-
- ); + return ; }; -const FormWrapper = (props: FormProps) => { +export const FormWrapper = (props: FormWrapperProps) => { + const { data, schema, dmmfSchema, resource } = props; + const { options } = useConfig(); + + const modelOptions: ModelOptions[typeof resource] = + options?.model?.[resource]; + const { edit, id, ...schemas } = useMemo( + () => + getSchemas( + data, + schema, + dmmfSchema, + modelOptions?.edit?.fields as EditFieldsOptions + ), + [] + ); + return ( <> - -
- + + +
+
+ +
+ +
+
+
+
+
); }; - -export default FormWrapper; diff --git a/packages/next-admin/src/components/FormHeader.tsx b/packages/next-admin/src/components/FormHeader.tsx index 43673dd3..35e95498 100644 --- a/packages/next-admin/src/components/FormHeader.tsx +++ b/packages/next-admin/src/components/FormHeader.tsx @@ -5,7 +5,7 @@ import { useConfig } from "../context/ConfigContext"; import { useI18n } from "../context/I18nContext"; import { EditFieldsOptions, - FormProps, + FormWrapperProps, ModelOptions, Permission, } from "../types"; @@ -24,7 +24,7 @@ export default function FormHeader({ data, schema, dmmfSchema, -}: FormProps) { +}: FormWrapperProps) { const { t } = useI18n(); const { basePath, options } = useConfig(); const modelOptions: ModelOptions[typeof resource] = diff --git a/packages/next-admin/src/components/NextAdmin.tsx b/packages/next-admin/src/components/NextAdmin.tsx index 27c66986..a0b35873 100644 --- a/packages/next-admin/src/components/NextAdmin.tsx +++ b/packages/next-admin/src/components/NextAdmin.tsx @@ -3,7 +3,7 @@ import { AdminComponentProps, CustomUIProps } from "../types"; import { getSchemaForResource } from "../utils/jsonSchema"; import { getCustomInputs } from "../utils/options"; import Dashboard from "./Dashboard"; -import Form from "./Form"; +import { FormWrapper } from "./Form"; import List from "./List"; import { MainLayout } from "./MainLayout"; import PageLoader from "./PageLoader"; @@ -76,7 +76,7 @@ export function NextAdmin({ : getCustomInputs(resource!, options!); return ( - { - const { formData, onChange, name, disabled, schema, required, formContext } = - props; + const { formData, onChange, name, disabled, schema, required } = props; - const dmmfSchema = formContext.dmmfSchema as FormProps["dmmfSchema"]; + const childSchema = schema.items as JSONSchema7; - const dmmfField = dmmfSchema.find((field) => field.name === name); - - if (dmmfField?.kind === "scalar" && dmmfField?.isList) { + if ( + (childSchema.type === "string" || + childSchema.type === "number" || + childSchema.type === "integer") && + schema.type === "array" && + // @ts-expect-error + !childSchema.relation + ) { return ( { ); } - const options = - dmmfField?.kind === "enum" ? (schema.enum as Enumeration[]) : undefined; + const options = schema.enum as Enumeration[] | undefined; return ( { + const [isOpen, setIsOpen] = useState(false); + const [resource, id] = href?.split("/").slice(-2) ?? []; + const { attributes, listeners, @@ -65,20 +71,34 @@ const DndItem = ({ )} {label} - {(href || deletable) && ( -
- {!!href && ( - - - - )} - {deletable && ( -
onRemoveClick()}> - -
+ +
+
+ { + setIsOpen(true); + }} + className="text-nextadmin-content-default dark:text-dark-nextadmin-content-default h-5 w-5 cursor-pointer" + /> + {isOpen && ( + setIsOpen(false)} + /> )}
- )} + {!!href && ( + + + + )} + {deletable && ( +
onRemoveClick()}> + +
+ )} +
); }; diff --git a/packages/next-admin/src/components/inputs/EmbeddedForm/EmbeddedFormModal.tsx b/packages/next-admin/src/components/inputs/EmbeddedForm/EmbeddedFormModal.tsx new file mode 100644 index 00000000..3b9b9430 --- /dev/null +++ b/packages/next-admin/src/components/inputs/EmbeddedForm/EmbeddedFormModal.tsx @@ -0,0 +1,118 @@ +import { Transition, TransitionChild } from "@headlessui/react"; +import { XMarkIcon } from "@heroicons/react/24/outline"; +import { UiSchema } from "@rjsf/utils"; +import { Fragment, useEffect, useMemo, useState } from "react"; +import { useConfig } from "../../../context/ConfigContext"; +import { useI18n } from "../../../context/I18nContext"; +import { ModelName, Schema } from "../../../types"; +import { Form } from "../../Form"; +import { + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + DialogRoot, + DialogTitle, +} from "../../radix/Dialog"; + +const EmbeddedFormModal = ({ + resource, + id, + onClose, +}: { + resource: ModelName; + id: string; + onClose: () => void; +}) => { + const { t } = useI18n(); + const { apiBasePath } = useConfig(); + + const [resourceData, setResourceData] = useState<{ + data: FormData; + schema: Schema; + uiSchema: UiSchema; + } | null>(null); + useEffect(() => { + const fetchData = async () => { + const { data, schema, uiSchema } = await fetch( + `${apiBasePath}/${resource}/${id}` + ).then((res) => res.json()); + + setResourceData({ data, schema, uiSchema }); + }; + + fetchData(); + }, [apiBasePath, id, resource]); + + const { data, schema, uiSchema } = useMemo(() => { + console.log("resourceData", resourceData); + if (resourceData) { + return resourceData; + } + + return { + data: null, + schema: null, + uiSchema: null, + }; + }, [resourceData]); + + return ( + + + + + + + + +
+ +
+

+ {t("Edit")} {resource} +

+ + + +
+ {schema && ( + + )} +
+
+
+
+
+
+ ); +}; + +export default EmbeddedFormModal; diff --git a/packages/next-admin/src/context/CustomInputsContext.tsx b/packages/next-admin/src/context/CustomInputsContext.tsx new file mode 100644 index 00000000..0b37e99a --- /dev/null +++ b/packages/next-admin/src/context/CustomInputsContext.tsx @@ -0,0 +1,22 @@ +import React, { createContext, PropsWithChildren, useContext } from "react"; + +const CustomInputsContext = createContext< + Record | undefined +>({}); + +export const useCustomInputs = () => useContext(CustomInputsContext); + +const CustomInputsProvider = ({ + customInputs, + children, +}: PropsWithChildren<{ + customInputs: Record | undefined; +}>) => { + return ( + + {children} + + ); +}; + +export default CustomInputsProvider; diff --git a/packages/next-admin/src/types.ts b/packages/next-admin/src/types.ts index 21336159..7b16c914 100644 --- a/packages/next-admin/src/types.ts +++ b/packages/next-admin/src/types.ts @@ -1,10 +1,11 @@ import * as OutlineIcons from "@heroicons/react/24/outline"; import { Prisma, PrismaClient } from "@prisma/client"; import type { JSONSchema7 } from "json-schema"; +import { NextApiRequest } from "next"; import { NextRequest, NextResponse } from "next/server"; import type { ChangeEvent, ReactNode } from "react"; import type { PropertyValidationError } from "./exceptions/ValidationError"; -import { NextApiRequest } from "next"; +import { UiSchema } from "@rjsf/utils"; declare type JSONSchema7Definition = JSONSchema7 & { relation?: ModelName; @@ -1038,6 +1039,15 @@ export type CreateAppHandlerParams

= { }; export type FormProps = { + data: any; + schema: Schema; + uiSchema: UiSchema + resource: ModelName; + id?: string | number | undefined; + validation?: PropertyValidationError[]; +}; + +export type FormWrapperProps = { data: any; schema: any; dmmfSchema: readonly Prisma.DMMF.Field[]; diff --git a/packages/next-admin/src/utils/jsonSchema.ts b/packages/next-admin/src/utils/jsonSchema.ts index 0777440a..29fc5b38 100644 --- a/packages/next-admin/src/utils/jsonSchema.ts +++ b/packages/next-admin/src/utils/jsonSchema.ts @@ -1,9 +1,9 @@ import { Prisma } from "@prisma/client"; import { UiSchema } from "@rjsf/utils"; -import { EditFieldsOptions, Field, ModelName } from "../types"; +import { EditFieldsOptions, Field, ModelName, Schema } from "../types"; export type Schemas = { - schema: any; + schema: Schema; uiSchema: UiSchema; }; @@ -66,7 +66,7 @@ export function getSchemas( if (schema && dmmfSchema) { const idProperty = dmmfSchema.find((property) => property.isId); - edit = !!data?.[idProperty?.name ?? "id"]; + edit = !!data; id = data?.[idProperty?.name ?? "id"]; Object.keys(schema.properties).forEach((property) => { const dmmfProperty = dmmfSchema.find( @@ -88,7 +88,7 @@ export function getSchemas( dmmfProperty?.isUpdatedAt || disabledFields?.includes(dmmfProperty.name)) ) { - edit + data ? (uiSchema[property] = { ...uiSchema[property], "ui:disabled": true, diff --git a/packages/next-admin/src/utils/prisma.ts b/packages/next-admin/src/utils/prisma.ts index 59069c28..8cdca5ba 100644 --- a/packages/next-admin/src/utils/prisma.ts +++ b/packages/next-admin/src/utils/prisma.ts @@ -582,7 +582,6 @@ export const getDataItem = async ({ prisma, resource, options, - resourceId, locale, isAppDir, diff --git a/packages/next-admin/src/utils/props.ts b/packages/next-admin/src/utils/props.ts index f401b69e..5b976019 100644 --- a/packages/next-admin/src/utils/props.ts +++ b/packages/next-admin/src/utils/props.ts @@ -13,7 +13,6 @@ import { import { getCustomInputs } from "./options"; import { getDataItem, getMappedDataList } from "./prisma"; import { - applyVisiblePropertiesInSchema, getModelIdProperty, getPrismaModelForResource, getResourceFromParams, @@ -147,29 +146,26 @@ export async function getPropsFromParams({ const resourceId = getResourceIdFromParam(params[1], resource); const dmmfSchema = getPrismaModelForResource(resource); - const edit = options?.model?.[resource]?.edit as EditOptions< - typeof resource - >; + + const data = resourceId ? await getDataItem({ + prisma, + resource, + resourceId, + options, + locale, + isAppDir, + }) : undefined; let deepCopySchema = await transformSchema( resource, - edit, - options + options, + data )(cloneDeep(schema)); const customInputs = isAppDir ? getCustomInputs(resource, options) : undefined; if (resourceId !== undefined) { - const data = await getDataItem({ - prisma, - resource, - resourceId, - options, - locale, - isAppDir, - }); - const toStringFunction = getToStringForModel( options?.model?.[resource] ); @@ -177,8 +173,6 @@ export async function getPropsFromParams({ ? toStringFunction(data) : resourceId.toString(); - applyVisiblePropertiesInSchema(resource, edit, data, deepCopySchema); - return { ...defaultProps, resource, diff --git a/packages/next-admin/src/utils/server.ts b/packages/next-admin/src/utils/server.ts index 153863bf..a62517e6 100644 --- a/packages/next-admin/src/utils/server.ts +++ b/packages/next-admin/src/utils/server.ts @@ -906,43 +906,44 @@ export const formatSearchFields = (uri: string) => */ export const transformSchema = ( resource: M, - edit: EditOptions, - options?: NextAdminOptions -) => - pipe( + options?: NextAdminOptions, + data?: any +) => { + const edit = options?.model?.[resource]?.edit as EditOptions; + + return pipe( removeHiddenProperties(resource, edit), changeFormatInSchema(resource, edit), fillRelationInSchema(resource, options), fillDescriptionInSchema(resource, edit), addCustomProperties(resource, edit), - orderSchema(resource, options) + orderSchema(resource, options), + visiblePropertiesInSchema(resource, edit, data) ); - -export const applyVisiblePropertiesInSchema = ( - resource: M, - edit: EditOptions, - data: any, - schema: Schema -) => { - const modelName = resource; - const model = models.find((model) => model.name === modelName); - if (!model) return schema; - const display = edit?.display; - const fields = edit?.fields; - if (display) { - display.forEach((property) => { - if ( - schema.definitions?.[modelName]?.properties && - fields?.[property]?.visible?.(data) === false - ) { - // @ts-expect-error - delete schema.definitions[modelName].properties[property]; - } - }); - } - return schema; }; +export const visiblePropertiesInSchema = + (resource: M, edit: EditOptions, data?: any) => + (schema: Schema) => { + const modelName = resource; + const model = models.find((model) => model.name === modelName); + if (!model) return schema; + const display = edit?.display; + const fields = edit?.fields; + if (display) { + display.forEach((property) => { + if ( + schema.definitions?.[modelName]?.properties && + fields?.[property]?.visible?.(data) === false + ) { + // @ts-expect-error + delete schema.definitions[modelName].properties[property]; + } + }); + } + return schema; + }; + const fillDescriptionInSchema = ( resource: M, editOptions: EditOptions diff --git a/packages/next-admin/src/utils/tools.ts b/packages/next-admin/src/utils/tools.ts index e31919ce..1e0cc28f 100644 --- a/packages/next-admin/src/utils/tools.ts +++ b/packages/next-admin/src/utils/tools.ts @@ -37,6 +37,9 @@ export const extractSerializable = (obj: T): T => { } else if (obj === null) { return obj; } else if (typeof obj === "object") { + if(React.isValidElement(obj)) { + return obj; + } let newObj = {} as T; for (const key in obj) { if (obj.hasOwnProperty(key)) { From ad53fff0052d6381965abf523c15b9282e00460b Mon Sep 17 00:00:00 2001 From: Colin Regourd Date: Wed, 18 Sep 2024 16:49:05 +0200 Subject: [PATCH 2/5] stable modal displaying --- apps/example/options.tsx | 16 +++- .../prisma/json-schema/json-schema.json | 8 +- packages/next-admin/src/appHandler.ts | 32 +------ packages/next-admin/src/components/Form.tsx | 95 +++++++++--------- .../next-admin/src/components/FormHeader.tsx | 21 ++-- .../next-admin/src/components/MainLayout.tsx | 4 +- packages/next-admin/src/components/Menu.tsx | 23 +---- .../next-admin/src/components/NextAdmin.tsx | 20 ++-- .../src/components/inputs/ArrayField.tsx | 9 +- .../src/components/inputs/DndItem.tsx | 37 +++---- .../inputs/EmbeddedForm/EmbeddedFormModal.tsx | 96 ++++++++++++------- .../MultiSelect/MultiSelectDisplayList.tsx | 8 +- .../MultiSelectDisplayListItem.tsx | 11 +-- .../MultiSelect/MultiSelectDisplayTable.tsx | 13 +-- .../inputs/MultiSelect/MultiSelectItem.tsx | 9 +- .../inputs/MultiSelect/MultiSelectWidget.tsx | 24 ++--- .../src/components/inputs/SelectWidget.tsx | 55 ++++++++--- .../src/components/inputs/Selector.tsx | 4 +- .../next-admin/src/context/ConfigContext.tsx | 22 +++-- .../src/context/CustomInputsContext.tsx | 22 ----- .../src/context/FormDataContext.tsx | 4 + .../src/context/ResourceContext.tsx | 56 +++++++++++ .../src/hooks/useSearchPaginatedResource.ts | 17 ++-- packages/next-admin/src/types.ts | 55 +++++++---- packages/next-admin/src/utils/jsonSchema.ts | 83 ++++++++-------- packages/next-admin/src/utils/props.ts | 35 ++++--- packages/next-admin/src/utils/server.ts | 26 +++-- 27 files changed, 444 insertions(+), 361 deletions(-) delete mode 100644 packages/next-admin/src/context/CustomInputsContext.tsx create mode 100644 packages/next-admin/src/context/ResourceContext.tsx diff --git a/apps/example/options.tsx b/apps/example/options.tsx index 8ab18859..b8d83f40 100644 --- a/apps/example/options.tsx +++ b/apps/example/options.tsx @@ -106,9 +106,10 @@ export const options: NextAdminOptions = { posts: { display: "list", orderField: "order", + }, role: { - disabled: true, + //disabled: true, }, avatar: { format: "file", @@ -146,7 +147,7 @@ export const options: NextAdminOptions = { }, hooks: { beforeDb: async (data, mode, request) => { - const newPassword = data.newPassword; + const newPassword = data.password; if (newPassword) { data.hashedPassword = `hashed-${newPassword}`; } @@ -201,6 +202,17 @@ export const options: NextAdminOptions = { }, }, edit: { + styles: { + _form: "grid-cols-3 gap-4 md:grid-cols-4", + id: "col-span-2 row-start-1", + title: "col-span-2 row-start-1", + content: "col-span-4 row-start-2", + published: "col-span-4 md:col-span-2 row-start-3", + categories: "col-span-4 md:col-span-2 row-start-4", + author: "col-span-4 md:col-span-3 row-start-5", + rate: "col-span-3 row-start-6", + tags: "col-span-4 row-start-7", + }, fields: { content: { format: "richtext-html", diff --git a/apps/example/prisma/json-schema/json-schema.json b/apps/example/prisma/json-schema/json-schema.json index c3728f3b..5931f79f 100644 --- a/apps/example/prisma/json-schema/json-schema.json +++ b/apps/example/prisma/json-schema/json-schema.json @@ -11,7 +11,10 @@ "type": "string" }, "hashedPassword": { - "type": "string" + "type": [ + "string", + "null" + ] }, "name": { "type": [ @@ -76,8 +79,7 @@ } }, "required": [ - "email", - "hashedPassword" + "email" ] }, "Post": { diff --git a/packages/next-admin/src/appHandler.ts b/packages/next-admin/src/appHandler.ts index 93a94cce..1bf00722 100644 --- a/packages/next-admin/src/appHandler.ts +++ b/packages/next-admin/src/appHandler.ts @@ -1,22 +1,14 @@ -import { cloneDeep } from "lodash"; import { createEdgeRouter } from "next-connect"; import { NextRequest, NextResponse } from "next/server"; import { HookError } from "./exceptions/HookError"; import { handleOptionsSearch } from "./handlers/options"; import { deleteResource, submitResource } from "./handlers/resources"; -import { - CreateAppHandlerParams, - EditFieldsOptions, - Permission, - RequestContext, -} from "./types"; -import { getSchemaForResource, getSchemas } from "./utils/jsonSchema"; +import { CreateAppHandlerParams, Permission, RequestContext, Schema } from "./types"; import { hasPermission } from "./utils/permissions"; import { getDataItem } from "./utils/prisma"; import { formatId, getFormValuesFromFormData, - getPrismaModelForResource, getResourceFromParams, getResources, transformSchema, @@ -69,30 +61,14 @@ export const createHandler =

({ options, }); - let deepCopySchema = await transformSchema( + let { uiSchema, schema: modelSchema } = await transformSchema( resource, + schema, options, data - )(cloneDeep(schema)); - - const dmmfSchema = getPrismaModelForResource(resource); - - const editFieldOptions = options?.model?.[resource]?.edit - ?.fields as EditFieldsOptions; - - const modelSchema = - resource && schema - ? getSchemaForResource(deepCopySchema, resource) - : undefined; - - const { uiSchema } = getSchemas( - data, - modelSchema, - dmmfSchema?.fields ?? [], - editFieldOptions ); - return NextResponse.json({ data, schema: modelSchema, uiSchema }); + return NextResponse.json({ data, modelSchema, uiSchema }); } else if ( !id && hasPermission(options?.model?.[resource], Permission.CREATE) diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx index d1dddf07..d9335288 100644 --- a/packages/next-admin/src/components/Form.tsx +++ b/packages/next-admin/src/components/Form.tsx @@ -11,6 +11,7 @@ import { FieldTemplateProps, getSubmitButtonOptions, ObjectFieldTemplateProps, + RJSFSchema, SubmitButtonProps, } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv8"; @@ -29,17 +30,17 @@ import React, { } from "react"; import { twMerge } from "tailwind-merge"; import { useConfig } from "../context/ConfigContext"; -import CustomInputsProvider, { - useCustomInputs, -} from "../context/CustomInputsContext"; -import FormDataProvider, { useFormData } from "../context/FormDataContext"; +import FormDataProvider, { + FormDataConsumer, + useFormData, +} from "../context/FormDataContext"; import FormStateProvider, { useFormState } from "../context/FormStateContext"; import { useI18n } from "../context/I18nContext"; import { MessageProvider, useMessage } from "../context/MessageContext"; +import ResourceProvider, { useResource } from "../context/ResourceContext"; import { useDeleteAction } from "../hooks/useDeleteAction"; import { useRouterInternal } from "../hooks/useRouterInternal"; import { - EditFieldsOptions, Field, FormProps, FormWrapperProps, @@ -47,7 +48,6 @@ import { ModelOptions, Permission, } from "../types"; -import { getSchemas } from "../utils/jsonSchema"; import { formatLabel, slugify } from "../utils/tools"; import FormHeader from "./FormHeader"; import ArrayField from "./inputs/ArrayField"; @@ -88,16 +88,17 @@ const widgets: RjsfForm["props"]["widgets"] = { TextareaWidget: TextareaWidget, }; -export const Form = ({ +export const Form = ({ data, - schema, - uiSchema, - resource, id, validation: validationProp, -}: FormProps) => { + customInputs, + disabled = false, +}: FormProps) => { const [validation, setValidation] = useState(validationProp); const { basePath, options, apiBasePath } = useConfig(); + const { resource, modelSchema, uiSchema } = useResource(); + const modelOptions: ModelOptions[typeof resource] = options?.model?.[resource]; const canDelete = @@ -114,12 +115,11 @@ export const Form = ({ const { t } = useI18n(); const formRef = useRef(null); const [isPending, setIsPending] = useState(false); - const allDisabled = edit && !canEdit; + const allDisabled = (edit && !canEdit) || disabled; const { runDeletion } = useDeleteAction(resource); const { showMessage } = useMessage(); const { cleanAll } = useFormState(); const { setFormData } = useFormData(); - const customInputs = useCustomInputs(); useEffect(() => { if (!edit && !canCreate) { @@ -207,7 +207,7 @@ export const Form = ({

); }, - [isPending, id] + [isPending] ); const extraErrors: ErrorSchema | undefined = validation?.reduce( @@ -469,6 +469,7 @@ export const Form = ({ /> ); } + return ( // @ts-expect-error forwardRef((_props, ref) => { - const { formData } = useFormData(); - return ( { - setFormData(e.formData); - }} - formData={formData} + formData={data} validator={validator} extraErrors={extraErrors} fields={fields} @@ -566,49 +562,44 @@ export const Form = ({ return ; }; -export const FormWrapper = (props: FormWrapperProps) => { - const { data, schema, dmmfSchema, resource } = props; - const { options } = useConfig(); - - const modelOptions: ModelOptions[typeof resource] = - options?.model?.[resource]; - const { edit, id, ...schemas } = useMemo( - () => - getSchemas( - data, - schema, - dmmfSchema, - modelOptions?.edit?.fields as EditFieldsOptions - ), - [] - ); +export const FormWrapper = ( + props: FormWrapperProps +) => { + const { data, modelSchema, uiSchema, resource } = props; return ( <> - - - - + + + +
- + + {({ formData }) => { + return ( + + data={{ ...data, ...formData }} + validation={props.validation} + customInputs={props.customInputs} + /> + ); + }} +
-
-
-
+ + + ); }; diff --git a/packages/next-admin/src/components/FormHeader.tsx b/packages/next-admin/src/components/FormHeader.tsx index 35e95498..a3321746 100644 --- a/packages/next-admin/src/components/FormHeader.tsx +++ b/packages/next-admin/src/components/FormHeader.tsx @@ -3,13 +3,14 @@ import { PlusSmallIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; import { useConfig } from "../context/ConfigContext"; import { useI18n } from "../context/I18nContext"; +import { useResource } from "../context/ResourceContext"; +import { useRouterInternal } from "../hooks/useRouterInternal"; import { - EditFieldsOptions, FormWrapperProps, + ModelName, ModelOptions, Permission, } from "../types"; -import { getSchemas } from "../utils/jsonSchema"; import { slugify } from "../utils/tools"; import ActionsDropdown from "./ActionsDropdown"; import Breadcrumb from "./Breadcrumb"; @@ -20,25 +21,19 @@ export default function FormHeader({ slug, icon, actions, - resource, - data, - schema, - dmmfSchema, -}: FormWrapperProps) { +}: FormWrapperProps) { const { t } = useI18n(); const { basePath, options } = useConfig(); + const router = useRouterInternal(); + const { resource } = useResource(); const modelOptions: ModelOptions[typeof resource] = options?.model?.[resource]; const canCreate = !modelOptions?.permissions || modelOptions?.permissions?.includes(Permission.CREATE); - const { edit, id } = getSchemas( - data, - schema, - dmmfSchema, - modelOptions?.edit?.fields as EditFieldsOptions - ); + const [_resource, id] = router.pathname.split("/").slice(-2); + const edit = id && id !== "new"; const breadcrumItems = [ { diff --git a/packages/next-admin/src/components/MainLayout.tsx b/packages/next-admin/src/components/MainLayout.tsx index 9bc3cfb9..c82098d6 100644 --- a/packages/next-admin/src/components/MainLayout.tsx +++ b/packages/next-admin/src/components/MainLayout.tsx @@ -28,7 +28,6 @@ export const MainLayout = ({ options, apiBasePath, resourcesIdProperty, - dmmfSchema, }: PropsWithChildren) => { const mergedTranslations = merge({ ...defaultTranslations }, translations); const localePath = locale ? `/${locale}` : ""; @@ -39,9 +38,8 @@ export const MainLayout = ({ basePath={`${localePath}${basePath}`} isAppDir={isAppDir} apiBasePath={apiBasePath} - dmmfSchema={dmmfSchema} - resource={resource} resourcesIdProperty={resourcesIdProperty!} + resources={resources} > import("./ColorSchemeSwitch"), { ssr: false, }); -export type MenuProps = { - resource?: ModelName; - resources?: ModelName[]; - resourcesTitles?: Record; - customPages?: AdminComponentProps["customPages"]; - configuration?: SidebarConfiguration; - resourcesIcons: AdminComponentProps["resourcesIcons"]; - user?: AdminComponentProps["user"]; - externalLinks?: AdminComponentProps["externalLinks"]; - title?: string; -}; - -export default function Menu({ +export default function Menu({ resources, resource: currentResource, resourcesTitles, @@ -67,7 +50,7 @@ export default function Menu({ user, externalLinks, title, -}: MenuProps) { +}: MenuProps) { const [sidebarOpen, setSidebarOpen] = useState(false); const { basePath } = useConfig(); const { pathname } = useRouterInternal(); diff --git a/packages/next-admin/src/components/NextAdmin.tsx b/packages/next-admin/src/components/NextAdmin.tsx index a0b35873..54796ae0 100644 --- a/packages/next-admin/src/components/NextAdmin.tsx +++ b/packages/next-admin/src/components/NextAdmin.tsx @@ -1,6 +1,5 @@ import dynamic from "next/dynamic"; -import { AdminComponentProps, CustomUIProps } from "../types"; -import { getSchemaForResource } from "../utils/jsonSchema"; +import { AdminComponentProps, CustomUIProps, ModelName } from "../types"; import { getCustomInputs } from "../utils/options"; import Dashboard from "./Dashboard"; import { FormWrapper } from "./Form"; @@ -11,16 +10,17 @@ import PageLoader from "./PageLoader"; const Head = dynamic(() => import("next/head")); // Components -export function NextAdmin({ +export function NextAdmin({ basePath, apiBasePath, data, resource, schema, + modelSchema, + uiSchema, resources, slug, total, - dmmfSchema, dashboard, validation, isAppDir, @@ -47,9 +47,6 @@ export function NextAdmin({ const actions = actionsProp || (resource ? options?.model?.[resource]?.actions : undefined); - const modelSchema = - resource && schema ? getSchemaForResource(schema, resource) : undefined; - const resourceTitle = resourcesTitles?.[resource!] ?? resource; const resourceIcon = resourcesIcons?.[resource!]; @@ -70,7 +67,7 @@ export function NextAdmin({ ); } - if ((data && !Array.isArray(data)) || (modelSchema && !data)) { + if ((data && !Array.isArray(data)) || (schema && !data)) { const customInputs = isAppDir ? customInputsProp : getCustomInputs(resource!, options!); @@ -79,14 +76,14 @@ export function NextAdmin({ ); @@ -124,7 +121,6 @@ export function NextAdmin({ user={user} externalLinks={externalLinks} options={options} - dmmfSchema={dmmfSchema} resourcesIdProperty={resourcesIdProperty!} > {renderMainComponent()} diff --git a/packages/next-admin/src/components/inputs/ArrayField.tsx b/packages/next-admin/src/components/inputs/ArrayField.tsx index 00900cf7..e012976a 100644 --- a/packages/next-admin/src/components/inputs/ArrayField.tsx +++ b/packages/next-admin/src/components/inputs/ArrayField.tsx @@ -1,6 +1,11 @@ import { FieldProps } from "@rjsf/utils"; import { JSONSchema7 } from "json-schema"; -import type { Enumeration } from "../../types"; +import type { + Enumeration, + Field, + ModelName, + SchemaProperty, +} from "../../types"; import MultiSelectWidget from "./MultiSelect/MultiSelectWidget"; import ScalarArrayField from "./ScalarArray/ScalarArrayField"; @@ -37,7 +42,7 @@ const ArrayField = (props: FieldProps) => { name={name} disabled={disabled ?? false} required={required} - schema={schema} + propertySchema={schema as SchemaProperty[Field]} options={options} /> ); diff --git a/packages/next-admin/src/components/inputs/DndItem.tsx b/packages/next-admin/src/components/inputs/DndItem.tsx index b87cef7b..9ab6f79c 100644 --- a/packages/next-admin/src/components/inputs/DndItem.tsx +++ b/packages/next-admin/src/components/inputs/DndItem.tsx @@ -9,6 +9,7 @@ import { import clsx from "clsx"; import Link from "next/link"; import { ReactElement, useState } from "react"; +import { useResource } from "../../context/ResourceContext"; import { ModelName } from "../../types"; import EmbeddedFormModal from "./EmbeddedForm/EmbeddedFormModal"; @@ -31,6 +32,7 @@ const DndItem = ({ }: Props) => { const [isOpen, setIsOpen] = useState(false); const [resource, id] = href?.split("/").slice(-2) ?? []; + const { resource: originalResource } = useResource(); const { attributes, @@ -73,25 +75,26 @@ const DndItem = ({
-
- { - setIsOpen(true); - }} - className="text-nextadmin-content-default dark:text-dark-nextadmin-content-default h-5 w-5 cursor-pointer" + {isOpen && ( + setIsOpen(false)} /> - {isOpen && ( - setIsOpen(false)} - /> - )} -
+ )} {!!href && ( - - - + <> + { + setIsOpen(true); + }} + className="text-nextadmin-content-default dark:text-dark-nextadmin-content-default h-5 w-5 cursor-pointer" + /> + + + + )} {deletable && (
onRemoveClick()}> diff --git a/packages/next-admin/src/components/inputs/EmbeddedForm/EmbeddedFormModal.tsx b/packages/next-admin/src/components/inputs/EmbeddedForm/EmbeddedFormModal.tsx index 3b9b9430..c233eb20 100644 --- a/packages/next-admin/src/components/inputs/EmbeddedForm/EmbeddedFormModal.tsx +++ b/packages/next-admin/src/components/inputs/EmbeddedForm/EmbeddedFormModal.tsx @@ -1,10 +1,12 @@ import { Transition, TransitionChild } from "@headlessui/react"; -import { XMarkIcon } from "@heroicons/react/24/outline"; +import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline"; import { UiSchema } from "@rjsf/utils"; -import { Fragment, useEffect, useMemo, useState } from "react"; +import { Fragment, useEffect, useState } from "react"; +import Loader from "../../../assets/icons/Loader"; import { useConfig } from "../../../context/ConfigContext"; import { useI18n } from "../../../context/I18nContext"; -import { ModelName, Schema } from "../../../types"; +import ResourceProvider from "../../../context/ResourceContext"; +import { Field, ModelName, SchemaModel } from "../../../types"; import { Form } from "../../Form"; import { DialogClose, @@ -15,12 +17,14 @@ import { DialogTitle, } from "../../radix/Dialog"; -const EmbeddedFormModal = ({ +const EmbeddedFormModal = ({ + originalResource, resource, id, onClose, }: { - resource: ModelName; + originalResource: O; + resource: M; id: string; onClose: () => void; }) => { @@ -29,34 +33,42 @@ const EmbeddedFormModal = ({ const [resourceData, setResourceData] = useState<{ data: FormData; - schema: Schema; + modelSchema: SchemaModel; uiSchema: UiSchema; } | null>(null); useEffect(() => { const fetchData = async () => { - const { data, schema, uiSchema } = await fetch( + const { data, modelSchema, uiSchema } = (await fetch( `${apiBasePath}/${resource}/${id}` - ).then((res) => res.json()); + ).then((res) => res.json())) as { + data: FormData; + modelSchema: SchemaModel; + uiSchema: UiSchema; + }; - setResourceData({ data, schema, uiSchema }); + uiSchema["ui:submitButtonOptions"] = { + norender: true, + }; + const disabledFields = ( + Object.keys(modelSchema.properties) as Field[] + ).filter( + (key) => + modelSchema.properties[key]?.items?.relation === originalResource || + modelSchema.properties[key]?.relation === originalResource + ); + + disabledFields.forEach((field) => { + uiSchema[field as string] = { + "ui:disabled": true, + }; + }); + + setResourceData({ data, modelSchema, uiSchema }); }; fetchData(); }, [apiBasePath, id, resource]); - const { data, schema, uiSchema } = useMemo(() => { - console.log("resourceData", resourceData); - if (resourceData) { - return resourceData; - } - - return { - data: null, - schema: null, - uiSchema: null, - }; - }, [resourceData]); - return ( @@ -86,25 +98,39 @@ const EmbeddedFormModal = ({ > -
- -
+ +

{t("Edit")} {resource}

- - - +
+ + + + +
+ +
+
+
- {schema && ( - + +
+ {resourceData ? ( + + modelSchema={resourceData.modelSchema} + uiSchema={resourceData.uiSchema} + > + + + ) : ( +
+ +
)}
diff --git a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectDisplayList.tsx b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectDisplayList.tsx index eed80d53..010ff249 100644 --- a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectDisplayList.tsx +++ b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectDisplayList.tsx @@ -1,12 +1,12 @@ import { DndContext, DragEndEvent } from "@dnd-kit/core"; import { SortableContext } from "@dnd-kit/sortable"; import { RJSFSchema } from "@rjsf/utils"; -import { Enumeration } from "../../../types"; +import { Enumeration, Field, ModelName, SchemaProperty } from "../../../types"; import MultiSelectDisplayListItem from "./MultiSelectDisplayListItem"; type Props = { formData: any; - schema: RJSFSchema; + propertySchema: SchemaProperty[Field]; onRemoveClick: (value: Enumeration["value"]) => void; deletable: boolean; sortable?: boolean; @@ -15,7 +15,7 @@ type Props = { const MultiSelectDisplayList = ({ formData, - schema, + propertySchema, onRemoveClick, deletable = true, sortable = false, @@ -48,7 +48,7 @@ const MultiSelectDisplayList = ({ onRemoveClick={onRemoveClick} deletable={deletable} sortable={sortable} - schema={schema} + propertySchema={propertySchema} /> ); })} diff --git a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectDisplayListItem.tsx b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectDisplayListItem.tsx index 57eb0054..fae8d06f 100644 --- a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectDisplayListItem.tsx +++ b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectDisplayListItem.tsx @@ -1,6 +1,5 @@ -import { RJSFSchema } from "@rjsf/utils"; import { useConfig } from "../../../context/ConfigContext"; -import { Enumeration } from "../../../types"; +import { Enumeration, Field, ModelName, SchemaProperty } from "../../../types"; import { slugify } from "../../../utils/tools"; import DndItem from "../DndItem"; @@ -9,7 +8,7 @@ type Props = { deletable?: boolean; sortable?: boolean; onRemoveClick: (value: Enumeration["value"]) => void; - schema: RJSFSchema; + propertySchema: SchemaProperty[Field]; }; const MultiSelectDisplayListItem = ({ @@ -17,13 +16,13 @@ const MultiSelectDisplayListItem = ({ deletable, sortable, onRemoveClick, - schema, + propertySchema, }: Props) => { const { basePath } = useConfig(); const { value, label } = item; - // @ts-expect-error - const relationModel = item?.data?.modelName ?? schema.items?.relation; + const relationModel = + item?.data?.modelName ?? propertySchema?.items?.relation; return ( [Field]; onRemoveClick: (value: any) => void; deletable: boolean; }; const MultiSelectDisplayTable = ({ formData, - schema, + propertySchema, deletable, onRemoveClick, }: Props) => { @@ -22,8 +21,7 @@ const MultiSelectDisplayTable = ({ data: formData?.map((data) => data.data), sortable: false, resourcesIdProperty: resourcesIdProperty!, - // @ts-expect-error - resource: schema.items?.relation, + resource: propertySchema!.items!.relation!, }); return ( @@ -31,8 +29,7 @@ const MultiSelectDisplayTable = ({ data.data)} - // @ts-expect-error - resource={schema.items?.relation} + resource={propertySchema!.items!.relation!} resourcesIdProperty={resourcesIdProperty!} rowSelection={{}} deletable={deletable} diff --git a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectItem.tsx b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectItem.tsx index 454b6026..e4608f7f 100644 --- a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectItem.tsx +++ b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectItem.tsx @@ -2,26 +2,25 @@ import { XMarkIcon } from "@heroicons/react/24/outline"; import { RJSFSchema } from "@rjsf/utils"; import Link from "next/link"; import { useConfig } from "../../../context/ConfigContext"; -import { Enumeration } from "../../../types"; +import { Enumeration, Field, ModelName, SchemaProperty } from "../../../types"; import { slugify } from "../../../utils/tools"; type Props = { onRemoveClick: (value: any) => void; deletable?: boolean; item: Enumeration; - schema: RJSFSchema; + propertySchema: SchemaProperty[Field]; }; const MultiSelectItem = ({ item, onRemoveClick, - schema, + propertySchema, deletable = true, }: Props) => { const { basePath } = useConfig(); - // @ts-expect-error - const relationModel = item?.data?.modelName ?? schema.items?.relation; + const relationModel = item?.data?.modelName ?? propertySchema?.items?.relation; return (
diff --git a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectWidget.tsx b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectWidget.tsx index 28419278..9b3ed02c 100644 --- a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectWidget.tsx +++ b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectWidget.tsx @@ -1,12 +1,12 @@ -import { RJSFSchema } from "@rjsf/utils"; import clsx from "clsx"; import DoubleArrow from "../../../assets/icons/DoubleArrow"; import { useConfig } from "../../../context/ConfigContext"; import { useFormState } from "../../../context/FormStateContext"; import { useI18n } from "../../../context/I18nContext"; +import { useResource } from "../../../context/ResourceContext"; import useClickOutside from "../../../hooks/useCloseOnOutsideClick"; import { useDisclosure } from "../../../hooks/useDisclosure"; -import { Enumeration, Field, ModelName } from "../../../types"; +import { Enumeration, Field, ModelName, SchemaProperty } from "../../../types"; import Button from "../../radix/Button"; import { Selector } from "../Selector"; import MultiSelectDisplayList from "./MultiSelectDisplayList"; @@ -20,14 +20,15 @@ type Props = { name: string; disabled: boolean; required?: boolean; - schema: RJSFSchema; + propertySchema: SchemaProperty[Field]; }; const MultiSelectWidget = (props: Props) => { - const { options: globalOptions, resource } = useConfig(); + const { options: globalOptions } = useConfig(); + const { resource } = useResource(); const { onToggle, isOpen, onClose } = useDisclosure(); const containerRef = useClickOutside(() => onClose()); - const { formData, onChange, options, name, schema } = props; + const { formData, onChange, options, name, propertySchema } = props; const { t } = useI18n(); const { setFieldDirty } = useFormState(); const fieldOptions = @@ -42,12 +43,13 @@ const MultiSelectWidget = (props: Props) => { const displayMode = !!fieldOptions && "display" in fieldOptions - ? fieldOptions.display ?? "select" + ? (fieldOptions.display ?? "select") : "select"; const fieldSortable = + displayMode === "list" && // @ts-expect-error - displayMode === "list" && (!!fieldOptions?.orderField || !!schema.enum); + (!!fieldOptions?.orderField || !!propertySchema?.enum); const select = ( + ); }; diff --git a/apps/example/components/UserDetailsDialogContent.tsx b/apps/example/components/UserDetailsDialogContent.tsx index e6ac8df0..29110d16 100644 --- a/apps/example/components/UserDetailsDialogContent.tsx +++ b/apps/example/components/UserDetailsDialogContent.tsx @@ -6,18 +6,20 @@ type Props = ClientActionDialogContentProps<"User">; const UserDetailsDialog = ({ data, onClose }: Props) => { return ( -
-
-

- {data?.email.value as string} -

-

- {data?.name.value as string} -

-

- {data?.role.value as string} -

-
+
+ {data?.map((user) => ( +
+

+ {user.email as string} +

+

+ {user.name as string} +

+

+ {user.role as string} +

+
+ ))}