diff --git a/apps/example/options.tsx b/apps/example/options.tsx index 3138f5c5..2d1c678c 100644 --- a/apps/example/options.tsx +++ b/apps/example/options.tsx @@ -84,6 +84,7 @@ export const options: NextAdminOptions = { } as const, "email", "posts", + "coPosts", "role", "birthDate", "avatar", @@ -121,6 +122,9 @@ export const options: NextAdminOptions = { display: "list", orderField: "order", }, + role: { + //disabled: true, + }, avatar: { format: "file", handler: { @@ -157,7 +161,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}`; } @@ -277,6 +281,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", @@ -297,6 +312,7 @@ export const options: NextAdminOptions = { "published", "categories", "author", + "coAuthors", "rate", "tags", ], diff --git a/apps/example/prisma/json-schema/json-schema.json b/apps/example/prisma/json-schema/json-schema.json new file mode 100644 index 00000000..1aae53cc --- /dev/null +++ b/apps/example/prisma/json-schema/json-schema.json @@ -0,0 +1,250 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "email": { + "type": "string" + }, + "hashedPassword": { + "type": [ + "string", + "null" + ] + }, + "name": { + "type": [ + "string", + "null" + ] + }, + "posts": { + "type": "array", + "items": { + "$ref": "#/definitions/Post" + } + }, + "coPosts": { + "type": "array", + "items": { + "$ref": "#/definitions/Post" + } + }, + "profile": { + "anyOf": [ + { + "$ref": "#/definitions/Profile" + }, + { + "type": "null" + } + ] + }, + "birthDate": { + "type": [ + "string", + "null" + ], + "format": "date-time" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + }, + "role": { + "type": "string", + "default": "USER", + "enum": [ + "USER", + "ADMIN" + ] + }, + "avatar": { + "type": [ + "string", + "null" + ] + }, + "metadata": { + "type": [ + "number", + "string", + "boolean", + "object", + "array", + "null" + ] + } + }, + "required": [ + "email" + ] + }, + "Post": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "title": { + "type": "string" + }, + "content": { + "type": [ + "string", + "null" + ] + }, + "published": { + "type": "boolean", + "default": false + }, + "author": { + "$ref": "#/definitions/User" + }, + "coAuthors": { + "type": "array", + "items": { + "$ref": "#/definitions/User" + } + }, + "categories": { + "type": "array", + "items": { + "$ref": "#/definitions/CategoriesOnPosts" + } + }, + "rate": { + "type": [ + "number", + "null" + ] + }, + "order": { + "type": "integer", + "default": 0 + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "title", + "authorId", + "tags" + ] + }, + "Profile": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "bio": { + "type": [ + "string", + "null" + ] + }, + "user": { + "anyOf": [ + { + "$ref": "#/definitions/User" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [] + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "name": { + "type": "string" + }, + "posts": { + "type": "array", + "items": { + "$ref": "#/definitions/CategoriesOnPosts" + } + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + }, + "required": [ + "name" + ] + }, + "CategoriesOnPosts": { + "type": "object", + "properties": { + "id": { + "type": "integer" + }, + "post": { + "$ref": "#/definitions/Post" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "order": { + "type": "integer", + "default": 0 + } + }, + "required": [ + "postId", + "categoryId" + ] + } + }, + "type": "object", + "properties": { + "user": { + "$ref": "#/definitions/User" + }, + "post": { + "$ref": "#/definitions/Post" + }, + "profile": { + "$ref": "#/definitions/Profile" + }, + "category": { + "$ref": "#/definitions/Category" + }, + "categoriesOnPosts": { + "$ref": "#/definitions/CategoriesOnPosts" + } + } +} \ No newline at end of file diff --git a/apps/example/prisma/migrations/20240919142125_add_coauthors/migration.sql b/apps/example/prisma/migrations/20240919142125_add_coauthors/migration.sql new file mode 100644 index 00000000..e5c69b9b --- /dev/null +++ b/apps/example/prisma/migrations/20240919142125_add_coauthors/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "_coAuthors" ( + "A" INTEGER NOT NULL, + "B" INTEGER NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_coAuthors_AB_unique" ON "_coAuthors"("A", "B"); + +-- CreateIndex +CREATE INDEX "_coAuthors_B_index" ON "_coAuthors"("B"); + +-- AddForeignKey +ALTER TABLE "_coAuthors" ADD CONSTRAINT "_coAuthors_A_fkey" FOREIGN KEY ("A") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_coAuthors" ADD CONSTRAINT "_coAuthors_B_fkey" FOREIGN KEY ("B") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/example/prisma/schema.prisma b/apps/example/prisma/schema.prisma index d0e2fd3d..7179d30b 100644 --- a/apps/example/prisma/schema.prisma +++ b/apps/example/prisma/schema.prisma @@ -27,6 +27,7 @@ model User { hashedPassword String? name String? posts Post[] @relation("author") // One-to-many relation + coPosts Post[] @relation("coAuthors") // Many-to-many relation profile Profile? @relation("profile") // One-to-one relation birthDate DateTime? createdAt DateTime @default(now()) @@ -43,6 +44,7 @@ model Post { published Boolean @default(false) author User @relation("author", fields: [authorId], references: [id]) // Many-to-one relation authorId Int + coAuthors User[] @relation("coAuthors") // Many-to-many relation categories CategoriesOnPosts[] rate Decimal? @db.Decimal(5, 2) order Int @default(0) diff --git a/packages/next-admin/src/appHandler.ts b/packages/next-admin/src/appHandler.ts index ca6bd4b9..a9a4f1a1 100644 --- a/packages/next-admin/src/appHandler.ts +++ b/packages/next-admin/src/appHandler.ts @@ -11,7 +11,7 @@ import { ServerAction, } from "./types"; import { hasPermission } from "./utils/permissions"; -import { getRawData } from "./utils/prisma"; +import { getDataItem, getRawData } from "./utils/prisma"; import { formatId, getFormValuesFromFormData, @@ -80,6 +80,50 @@ export const createHandler =

({ return NextResponse.json(data); }) + .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)) || + (!id && hasPermission(options?.model?.[resource], Permission.CREATE)) + ) { + let data; + if (id) { + data = await getDataItem({ + prisma, + resource, + resourceId: id, + options, + }); + } + + let { uiSchema, schema: modelSchema } = await transformSchema( + resource, + schema, + options, + data + ); + + return NextResponse.json({ data: data ?? {}, modelSchema, uiSchema }); + } 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)!; @@ -151,6 +195,18 @@ export const createHandler =

({ ? formatId(resource, ctx.params[paramKey].at(-1)!) : undefined; + console.log("id", id); + + if ( + (id && !hasPermission(options?.model?.[resource], Permission.EDIT)) || + (!id && !hasPermission(options?.model?.[resource], Permission.CREATE)) + ) { + return NextResponse.json( + { error: "You don't have permission to edit this resource" }, + { status: 403 } + ); + } + const editOptions = options?.model?.[resource]?.edit; const mode = !!id ? "edit" : "create"; diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx index a39c24fd..2c0e035d 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"; @@ -30,21 +31,24 @@ import React, { import { twMerge } from "tailwind-merge"; import ClientActionDialogProvider from "../context/ClientActionDialogContext"; import { useConfig } from "../context/ConfigContext"; -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, ModelName, ModelOptions, Permission, } from "../types"; -import { getSchemas } from "../utils/jsonSchema"; import { formatLabel, slugify } from "../utils/tools"; import FormHeader from "./FormHeader"; import ArrayField from "./inputs/ArrayField"; @@ -85,15 +89,17 @@ const widgets: RjsfForm["props"]["widgets"] = { TextareaWidget: TextareaWidget, }; -const Form = ({ +export const Form = ({ data, schema, - resource, validation: validationProp, customInputs, -}: FormProps) => { + 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 = @@ -105,16 +111,12 @@ const Form = ({ const canCreate = !modelOptions?.permissions || modelOptions?.permissions?.includes(Permission.CREATE); - const { edit, id, ...schemas } = getSchemas( - data, - schema, - modelOptions?.edit?.fields as EditFieldsOptions - ); const { router } = useRouterInternal(); + const edit = !!data; 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(); @@ -206,7 +208,7 @@ const Form = ({ ); }, - [isPending, id] + [isPending] ); const extraErrors: ErrorSchema | undefined = validation?.reduce( @@ -468,6 +470,7 @@ const Form = ({ /> ); } + return ( // @ts-expect-error forwardRef((_props, ref) => { - const { formData } = useFormData(); - return ( { - setFormData(e.formData); - }} - formData={formData} + schema={modelSchema as RJSFSchema} + uiSchema={uiSchema} + formData={data} validator={validator} extraErrors={extraErrors} fields={fields} @@ -561,31 +560,56 @@ const Form = ({ [submitButton] ); - return ( -

-
- -
- -
-
-
- ); + return ; }; -const FormWrapper = (props: FormProps) => { +export const FormWrapper = ( + props: FormWrapperProps +) => { + const { pathname } = useRouterInternal(); + const { data, modelSchema, uiSchema, resource, schema } = props; + const [id] = + pathname.split("/").slice(-1)[0] !== "new" + ? pathname.split("/").slice(-1) + : [undefined]; + return ( - + + - - -
- - + + + +
+
+ +
+ + {({ formData }) => { + return ( + + data={{ ...data, ...formData }} + id={id} + validation={props.validation} + customInputs={props.customInputs} + schema={schema} + /> + ); + }} + +
+
+
+
+
+ - + + ); }; - -export default FormWrapper; diff --git a/packages/next-admin/src/components/FormHeader.tsx b/packages/next-admin/src/components/FormHeader.tsx index e67b586a..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, - FormProps, + 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,23 +21,19 @@ export default function FormHeader({ slug, icon, actions, - resource, - data, - schema, -}: FormProps) { +}: 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, - 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 aed03f0d..e71088ad 100644 --- a/packages/next-admin/src/components/MainLayout.tsx +++ b/packages/next-admin/src/components/MainLayout.tsx @@ -39,8 +39,8 @@ export const MainLayout = ({ basePath={`${localePath}${basePath}`} isAppDir={isAppDir} apiBasePath={apiBasePath} - resource={resource} resourcesIdProperty={resourcesIdProperty!} + resources={resources} schema={schema} > diff --git a/packages/next-admin/src/components/Menu.tsx b/packages/next-admin/src/components/Menu.tsx index 52214c7a..3250df72 100644 --- a/packages/next-admin/src/components/Menu.tsx +++ b/packages/next-admin/src/components/Menu.tsx @@ -21,12 +21,7 @@ import Image from "next/image"; import { useConfig } from "../context/ConfigContext"; import { useI18n } from "../context/I18nContext"; import { useRouterInternal } from "../hooks/useRouterInternal"; -import { - AdminComponentProps, - ModelIcon, - ModelName, - SidebarConfiguration, -} from "../types"; +import { MenuProps, ModelIcon, ModelName } from "../types"; import { slugify } from "../utils/tools"; import Divider from "./Divider"; import ResourceIcon from "./common/ResourceIcon"; @@ -45,19 +40,7 @@ const ColorSchemeSwitch = dynamic(() => 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 e41f6814..0ca55c3b 100644 --- a/packages/next-admin/src/components/NextAdmin.tsx +++ b/packages/next-admin/src/components/NextAdmin.tsx @@ -1,10 +1,10 @@ import { merge } from "lodash"; import dynamic from "next/dynamic"; import { AdminComponentProps, CustomUIProps } from "../types"; +import { getClientActionDialogs, getCustomInputs } from "../utils/options"; 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"; @@ -18,6 +18,8 @@ export function NextAdmin({ data, resource, schema, + modelSchema, + uiSchema, resources, slug, total, @@ -49,9 +51,6 @@ export function NextAdmin({ resource ? options?.model?.[resource]?.actions : [] ); - const modelSchema = - resource && schema ? getSchemaForResource(schema, resource) : undefined; - const resourceTitle = resourcesTitles?.[resource!] ?? resource; const resourceIcon = resourcesIcons?.[resource!]; @@ -78,11 +77,14 @@ export function NextAdmin({ : getCustomInputs(resource!, options!); return ( - { - const { formData, onChange, name, disabled, schema, required, formContext } = - props; + const {resource, modelSchema } = useResource(); + const { formData, onChange, name, disabled, schema, required } = props; - const resourceDefinition: FormProps["schema"] = formContext.schema; + const resourceDefinition: FormProps["schema"] = modelSchema!; const field = resourceDefinition.properties[ @@ -38,7 +46,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 62badac0..9ab6f79c 100644 --- a/packages/next-admin/src/components/inputs/DndItem.tsx +++ b/packages/next-admin/src/components/inputs/DndItem.tsx @@ -3,11 +3,15 @@ import { CSS } from "@dnd-kit/utilities"; import { ArrowTopRightOnSquareIcon, Bars2Icon, + PencilSquareIcon, XMarkIcon, } from "@heroicons/react/24/outline"; import clsx from "clsx"; import Link from "next/link"; -import { ReactElement } from "react"; +import { ReactElement, useState } from "react"; +import { useResource } from "../../context/ResourceContext"; +import { ModelName } from "../../types"; +import EmbeddedFormModal from "./EmbeddedForm/EmbeddedFormModal"; type Props = { label: string | ReactElement; @@ -26,6 +30,10 @@ const DndItem = ({ href, value, }: Props) => { + const [isOpen, setIsOpen] = useState(false); + const [resource, id] = href?.split("/").slice(-2) ?? []; + const { resource: originalResource } = useResource(); + const { attributes, listeners, @@ -65,20 +73,35 @@ const DndItem = ({ )} {label} - {(href || deletable) && ( -
- {!!href && ( + +
+ {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()}> - -
- )} -
- )} + + )} + {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..3324b085 --- /dev/null +++ b/packages/next-admin/src/components/inputs/EmbeddedForm/EmbeddedFormModal.tsx @@ -0,0 +1,144 @@ +import { Transition, TransitionChild } from "@headlessui/react"; +import { CheckIcon, XMarkIcon } from "@heroicons/react/24/outline"; +import { UiSchema } from "@rjsf/utils"; +import { Fragment, useEffect, useState } from "react"; +import Loader from "../../../assets/icons/Loader"; +import { useConfig } from "../../../context/ConfigContext"; +import { useI18n } from "../../../context/I18nContext"; +import ResourceProvider from "../../../context/ResourceContext"; +import { Field, ModelName, SchemaModel } from "../../../types"; +import { Form } from "../../Form"; +import { + DialogClose, + DialogContent, + DialogOverlay, + DialogPortal, + DialogRoot, + DialogTitle, +} from "../../radix/Dialog"; + +const EmbeddedFormModal = ({ + originalResource, + resource, + id, + onClose, +}: { + originalResource: O; + resource: M; + id?: string; + onClose: () => void; +}) => { + const { t } = useI18n(); + const { apiBasePath } = useConfig(); + + const [resourceData, setResourceData] = useState<{ + data: FormData; + modelSchema: SchemaModel; + uiSchema: UiSchema; + } | null>(null); + useEffect(() => { + const fetchData = async () => { + const { data, modelSchema, uiSchema } = (await fetch( + `${apiBasePath}/${resource}/${id ?? ""}` + ).then((res) => res.json())) as { + data: FormData; + modelSchema: SchemaModel; + uiSchema: 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]); + + return ( + + + + + + + + + +
+

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

+
+ + + + +
+ +
+
+
+
+
+ +
+ {resourceData ? ( + + + + ) : ( +
+ +
+ )} +
+
+
+
+
+
+ ); +}; + +export default EmbeddedFormModal; 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..085cee77 100644 --- a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectItem.tsx +++ b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectItem.tsx @@ -1,27 +1,26 @@ 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 5e8ffbd4..7ba0ba33 100644 --- a/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectWidget.tsx +++ b/packages/next-admin/src/components/inputs/MultiSelect/MultiSelectWidget.tsx @@ -1,13 +1,15 @@ -import { RJSFSchema } from "@rjsf/utils"; import clsx from "clsx"; +import { useState } from "react"; 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 EmbeddedFormModal from "../EmbeddedForm/EmbeddedFormModal"; import { Selector } from "../Selector"; import MultiSelectDisplayList from "./MultiSelectDisplayList"; import MultiSelectDisplayTable from "./MultiSelectDisplayTable"; @@ -20,19 +22,24 @@ 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 [isModalOpen, setIsOpen] = useState(false); 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 = globalOptions?.model?.[resource!]?.edit?.fields?.[name as Field]; + const relationModel = + propertySchema?.relation || propertySchema?.items?.relation; + const onRemoveClick = (value: any) => { setFieldDirty(name); onChange(formData?.filter((item: Enumeration) => item.value !== value)); @@ -46,8 +53,9 @@ const MultiSelectWidget = (props: Props) => { : "select"; const fieldSortable = + displayMode === "list" && // @ts-expect-error - displayMode === "list" && (!!fieldOptions?.orderField || !!schema.enum); + (!!fieldOptions?.orderField || !!propertySchema?.enum); const select = (