diff --git a/.changeset/sharp-eggs-hang.md b/.changeset/sharp-eggs-hang.md new file mode 100644 index 00000000..1ecab3d7 --- /dev/null +++ b/.changeset/sharp-eggs-hang.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Allow custom inputs in array field (#510) diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0b1213a0..ebffa6eb 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -46,7 +46,7 @@ jobs: pnpm turbo test:e2e cd apps/example && pnpm prisma db seed && cd - BASE_URL=http://localhost:3000/pagerouter/admin pnpm test:e2e - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 if: always() with: name: playwright-report diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx index a39c24fd..2269db5d 100644 --- a/packages/next-admin/src/components/Form.tsx +++ b/packages/next-admin/src/components/Form.tsx @@ -8,10 +8,11 @@ import RjsfForm from "@rjsf/core"; import { BaseInputTemplateProps, ErrorSchema, + FieldProps, FieldTemplateProps, getSubmitButtonOptions, ObjectFieldTemplateProps, - SubmitButtonProps, + SubmitButtonProps } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv8"; import clsx from "clsx"; @@ -25,7 +26,7 @@ import React, { useEffect, useMemo, useRef, - useState, + useState } from "react"; import { twMerge } from "tailwind-merge"; import ClientActionDialogProvider from "../context/ClientActionDialogContext"; @@ -71,11 +72,6 @@ const RichTextField = dynamic(() => import("./inputs/RichText/RichTextField"), { ssr: false, }); -const fields: RjsfForm["props"]["fields"] = { - ArrayField, - NullField, -}; - const widgets: RjsfForm["props"]["widgets"] = { DateWidget: DateWidget, DateTimeWidget: DateTimeWidget, @@ -309,6 +305,18 @@ const Form = ({ [apiBasePath, id] ); + const fields: RjsfForm["props"]["fields"] = { + ArrayField: (props: FieldProps) => { + const customInput = customInputs?.[props.name as Field] + const improvedCustomInput = customInput ? cloneElement(customInput, { + ...customInput.props, + mode: edit ? "edit" : "create", + }) : undefined; + return ArrayField({ ...props, customInput: improvedCustomInput }) + }, + NullField, + }; + const templates: RjsfForm["props"]["templates"] = { FieldTemplate: (props: FieldTemplateProps) => { const { @@ -501,6 +509,9 @@ const Form = ({ ) : null; }, + ButtonTemplates: { + SubmitButton: submitButton, + }, }; const CustomForm = forwardRef>( @@ -548,10 +559,7 @@ const Form = ({ fields={fields} disabled={allDisabled} formContext={{ isPending, schema }} - templates={{ - ...templates, - ButtonTemplates: { SubmitButton: submitButton }, - }} + templates={templates} widgets={widgets} ref={ref} className="relative" diff --git a/packages/next-admin/src/components/inputs/ArrayField.tsx b/packages/next-admin/src/components/inputs/ArrayField.tsx index a75144ba..728690c3 100644 --- a/packages/next-admin/src/components/inputs/ArrayField.tsx +++ b/packages/next-admin/src/components/inputs/ArrayField.tsx @@ -1,17 +1,18 @@ import { FieldProps } from "@rjsf/utils"; +import type { CustomInputProps, Enumeration, FormProps } from "../../types"; import MultiSelectWidget from "./MultiSelect/MultiSelectWidget"; import ScalarArrayField from "./ScalarArray/ScalarArrayField"; -import type { Enumeration, FormProps, ModelName } from "../../types"; -const ArrayField = (props: FieldProps) => { - const { formData, onChange, name, disabled, schema, required, formContext } = + +const ArrayField = (props: FieldProps & { customInput?: React.ReactElement }) => { + const { formData, onChange, name, disabled, schema, required, formContext, customInput } = props; const resourceDefinition: FormProps["schema"] = formContext.schema; const field = resourceDefinition.properties[ - name as keyof typeof resourceDefinition.properties + name as keyof typeof resourceDefinition.properties ]; if (field?.__nextadmin?.kind === "scalar" && field?.__nextadmin?.isList) { @@ -22,6 +23,7 @@ const ArrayField = (props: FieldProps) => { onChange={onChange} disabled={disabled ?? false} schema={schema} + customInput={customInput} /> ); } diff --git a/packages/next-admin/src/components/inputs/JsonField.tsx b/packages/next-admin/src/components/inputs/JsonField.tsx index 17adae31..9fc9647a 100644 --- a/packages/next-admin/src/components/inputs/JsonField.tsx +++ b/packages/next-admin/src/components/inputs/JsonField.tsx @@ -4,7 +4,7 @@ import { useMemo } from "react"; import { useColorScheme } from "../../context/ColorSchemeContext"; import { CustomInputProps } from "../../types"; -type Props = CustomInputProps; +type Props = Omit; const JsonField = ({ value, onChange, name, disabled, required }: Props) => { const { colorScheme } = useColorScheme(); diff --git a/packages/next-admin/src/components/inputs/ScalarArray/ScalarArrayField.tsx b/packages/next-admin/src/components/inputs/ScalarArray/ScalarArrayField.tsx index a1f9a63e..45ac5445 100644 --- a/packages/next-admin/src/components/inputs/ScalarArray/ScalarArrayField.tsx +++ b/packages/next-admin/src/components/inputs/ScalarArray/ScalarArrayField.tsx @@ -1,12 +1,12 @@ -import { RJSFSchema } from "@rjsf/utils"; -import Button from "../../radix/Button"; -import { useI18n } from "../../../context/I18nContext"; import { DndContext, DragEndEvent } from "@dnd-kit/core"; import { SortableContext } from "@dnd-kit/sortable"; +import { RJSFSchema } from "@rjsf/utils"; import { useEffect, useState } from "react"; import { useFormState } from "../../../context/FormStateContext"; +import { useI18n } from "../../../context/I18nContext"; +import { CustomInputProps } from "../../../types"; +import Button from "../../radix/Button"; import ScalarArrayFieldItem from "./ScalarArrayFieldItem"; -import clsx from "clsx"; type Scalar = number | string; @@ -16,6 +16,7 @@ type Props = { name: string; disabled: boolean; schema: RJSFSchema; + customInput?: React.ReactElement; }; const ScalarArrayField = ({ @@ -24,6 +25,7 @@ const ScalarArrayField = ({ name, disabled, schema, + customInput, }: Props) => { const { t } = useI18n(); const { setFieldDirty } = useFormState(); @@ -77,6 +79,7 @@ const ScalarArrayField = ({ id={value.id} // @ts-expect-error scalarType={schema.items.type} + customInput={customInput} /> ); }); diff --git a/packages/next-admin/src/components/inputs/ScalarArray/ScalarArrayFieldItem.tsx b/packages/next-admin/src/components/inputs/ScalarArray/ScalarArrayFieldItem.tsx index 4ef8d00e..a1c7cbaf 100644 --- a/packages/next-admin/src/components/inputs/ScalarArray/ScalarArrayFieldItem.tsx +++ b/packages/next-admin/src/components/inputs/ScalarArray/ScalarArrayFieldItem.tsx @@ -1,3 +1,6 @@ +import { ChangeEvent, cloneElement } from "react"; +import { twMerge } from "tailwind-merge"; +import { CustomInputProps } from "../../../types"; import BaseInput from "../BaseInput"; import DndItem from "../DndItem"; @@ -9,6 +12,7 @@ type Props = { inputValue: string; id: string; scalarType: string; + customInput?: React.ReactElement; }; const ScalarArrayFieldItem = ({ @@ -18,18 +22,16 @@ const ScalarArrayFieldItem = ({ onRemoveClick, id, scalarType, + customInput = , }: Props) => { - const renderInput = () => { - return ( - onChange(evt.target.value)} - disabled={disabled} - className="w-full" - type={scalarType === "number" ? "number" : "text"} - /> - ); - }; + const renderInput = () => cloneElement(customInput, { + ...customInput.props, + value, + onChange: (evt: ChangeEvent) => onChange(evt.target.value), + disabled, + className: twMerge("w-full", customInput.props.className), + type: scalarType === "number" ? "number" : "text", + }); return ( ; +}> & ComponentProps<"input">; export type TranslationKeys = | "actions.delete.label"