UNPKG

@premieroctet/next-admin

Version:

Next-Admin provides a customizable and turnkey admin dashboard for applications built with Next.js and powered by the Prisma ORM. It aims to simplify the development process by providing a turnkey admin system that can be easily integrated into your proje

474 lines (473 loc) 22.3 kB
"use client"; import { jsx, jsxs } from "react/jsx-runtime"; import { CheckCircleIcon, InformationCircleIcon, TrashIcon } from "@heroicons/react/24/outline"; import core_default from "@rjsf/core"; import validator_ajv8_default from "@rjsf/validator-ajv8"; import external_clsx_default from "clsx"; import external_react_default, { Suspense, cloneElement, forwardRef, lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { twMerge } from "tailwind-merge"; import ClientActionDialogContext_mjs_default from "../context/ClientActionDialogContext.mjs"; import { useConfig } from "../context/ConfigContext.mjs"; import FormDataContext_mjs_default, { useFormData } from "../context/FormDataContext.mjs"; import FormStateContext_mjs_default, { useFormState } from "../context/FormStateContext.mjs"; import { useI18n } from "../context/I18nContext.mjs"; import { useMessage } from "../context/MessageContext.mjs"; import { useDeleteAction } from "../hooks/useDeleteAction.mjs"; import { useRouterInternal } from "../hooks/useRouterInternal.mjs"; import { Permission } from "../types.mjs"; import { getSchemas } from "../utils/jsonSchema.mjs"; import { formatLabel, isFileUploadFormat, slugify } from "../utils/tools.mjs"; import external_FormHeader_mjs_default from "./FormHeader.mjs"; import ArrayField_mjs_default from "./inputs/ArrayField.mjs"; import BaseInput_mjs_default from "./inputs/BaseInput.mjs"; import CheckboxWidget_mjs_default from "./inputs/CheckboxWidget.mjs"; import DateTimeWidget_mjs_default from "./inputs/DateTimeWidget.mjs"; import DateWidget_mjs_default from "./inputs/DateWidget.mjs"; import FileWidget_mjs_default from "./inputs/FileWidget/FileWidget.mjs"; import JsonField_mjs_default from "./inputs/JsonField.mjs"; import NullField_mjs_default from "./inputs/NullField.mjs"; import SelectWidget_mjs_default from "./inputs/SelectWidget.mjs"; import TextareaWidget_mjs_default from "./inputs/TextareaWidget.mjs"; import { Message } from "./Message.mjs"; import Button_mjs_default from "./radix/Button.mjs"; import { TooltipContent, TooltipPortal, TooltipProvider, TooltipRoot, TooltipTrigger } from "./radix/Tooltip.mjs"; import { getSubmitButtonOptions } from "../utils/rjsf.mjs"; const RichTextField = /*#__PURE__*/ lazy(()=>import("./inputs/RichText/RichTextField.mjs")); const widgets = { DateWidget: DateWidget_mjs_default, DateTimeWidget: DateTimeWidget_mjs_default, SelectWidget: SelectWidget_mjs_default, CheckboxWidget: CheckboxWidget_mjs_default, FileWidget: FileWidget_mjs_default, TextareaWidget: TextareaWidget_mjs_default }; const Form = ({ data, schema, resource, validation: validationProp, customInputs })=>{ const [validation, setValidation] = useState(validationProp); const { basePath, options, apiBasePath } = useConfig(); const modelOptions = options?.model?.[resource]; const canDelete = !modelOptions?.permissions || modelOptions?.permissions?.includes(Permission.DELETE); const canEdit = !modelOptions?.permissions || modelOptions?.permissions?.includes(Permission.EDIT); const canCreate = !modelOptions?.permissions || modelOptions?.permissions?.includes(Permission.CREATE); const { edit, id, ...schemas } = getSchemas(data, schema, modelOptions?.edit?.fields); const { router } = useRouterInternal(); const { t } = useI18n(); const formRef = useRef(null); const [isPending, setIsPending] = useState(false); const allDisabled = edit && !canEdit; const { runSingleDeletion } = useDeleteAction(resource); const { showMessage } = useMessage(); const { cleanAll } = useFormState(); const { setFormData } = useFormData(); useEffect(()=>{ if (!edit && !canCreate) router.replace({ pathname: "/" }); }, [ canCreate, edit, router ]); const submitButton = useMemo(()=>(props)=>{ const { uiSchema } = props; const { norender, props: buttonProps } = getSubmitButtonOptions(uiSchema); if (norender) return null; return /*#__PURE__*/ jsxs("div", { className: "align-center mt-4 flex flex-wrap justify-between gap-2", children: [ (edit && canEdit || !edit && canCreate) && /*#__PURE__*/ jsxs("div", { className: "order-2 flex gap-2", children: [ /*#__PURE__*/ jsxs(Button_mjs_default, { ...buttonProps, className: "order-2 flex gap-2", type: "submit", name: "__admin_redirect", value: "list", loading: isPending, children: [ !isPending && /*#__PURE__*/ jsx(CheckCircleIcon, { className: "h-6 w-6" }), t("form.button.save.label") ] }), /*#__PURE__*/ jsx(Button_mjs_default, { ...buttonProps, variant: "ghost", className: "order-1 hidden sm:block", tabIndex: -1, type: "submit", loading: isPending, name: "save_edit", children: t("form.button.save_edit.label") }) ] }), /*#__PURE__*/ jsx("div", { className: "order-1", children: edit && canDelete && /*#__PURE__*/ jsxs(Button_mjs_default, { variant: "destructiveOutline", className: "flex gap-2", formNoValidate: true, tabIndex: -1, onClick: async (e)=>{ if (confirm(t("form.delete.alert"))) try { setIsPending(true); await runSingleDeletion(id); router.replace({ pathname: `${basePath}/${slugify(resource)}`, query: { message: JSON.stringify({ type: "success", message: t("form.delete.succeed") }) } }); } catch (e) { showMessage({ type: "error", message: t(e.message) }); } finally{ setIsPending(false); } else e.preventDefault(); }, loading: isPending, children: [ !isPending && /*#__PURE__*/ jsx(TrashIcon, { className: "h-4 w-4" }), t("form.button.delete.label") ] }) }) ] }); }, [ isPending, id ]); const extraErrors = validation?.reduce((acc, curr)=>{ acc[curr.property] = { __errors: [ curr.message ] }; return acc; }, {}); const onSubmit = useCallback(async (formData)=>{ try { setIsPending(true); const response = await fetch(`${apiBasePath}/${slugify(resource)}${id ? `/${id}` : ""}`, { method: "POST", body: formData }); const result = await response.json(); result?.validation ? setValidation(result.validation) : setValidation(void 0); if (result?.data) { setFormData(result.data); cleanAll(); } if (result?.deleted) return router.replace({ pathname: `${basePath}/${slugify(resource)}`, query: { message: JSON.stringify({ type: "success", message: t("form.delete.succeed") }) } }); if (result?.created) { const pathname = result?.redirect ? `${basePath}/${slugify(resource)}` : `${basePath}/${slugify(resource)}/${result.createdId}`; return router.push({ pathname, query: { message: JSON.stringify({ type: "success", message: t("form.create.succeed") }) } }); } if (result?.updated) { const pathname = result?.redirect ? `${basePath}/${slugify(resource)}` : location.pathname; if (pathname === location.pathname) { router.refresh(); showMessage({ type: "success", message: t("form.update.succeed") }); } else router.push({ pathname, query: { message: JSON.stringify({ type: "success", message: t("form.update.succeed") }) } }); } if (result?.error) showMessage({ type: "error", message: t(result.error) }); } finally{ setIsPending(false); } }, [ apiBasePath, id ]); const fields = useMemo(()=>({ ArrayField: (props)=>{ const customInput = customInputs?.[props.name]; const improvedCustomInput = customInput ? /*#__PURE__*/ cloneElement(customInput, { ...customInput.props, mode: edit ? "edit" : "create" }) : void 0; return /*#__PURE__*/ jsx(ArrayField_mjs_default, { ...props, customInput: improvedCustomInput }); }, NullField: NullField_mjs_default }), [ customInputs, edit ]); const templates = { FieldTemplate: (props)=>{ const { id, classNames, style, label, help, required, description, errors, children, schema } = props; const labelAlias = modelOptions?.aliases?.[id] || formatLabel(label); const labelName = t(`model.${resource}.fields.${id}`, {}, labelAlias); let styleField = modelOptions?.edit?.styles?.[id]; const tooltip = modelOptions?.edit?.fields?.[id]?.tooltip || modelOptions?.edit?.customFields?.[id]?.tooltip; const sanitizedClassNames = classNames?.split(",").filter((className)=>"null" !== className).join(" "); return /*#__PURE__*/ jsxs("div", { style: style, className: external_clsx_default(sanitizedClassNames, styleField), children: [ "null" !== schema.type && /*#__PURE__*/ jsxs("label", { className: external_clsx_default("text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted mb-2 flex items-center gap-2 text-sm font-medium leading-6"), htmlFor: id, children: [ labelName, required ? "*" : null, !!tooltip && /*#__PURE__*/ jsx(TooltipProvider, { children: /*#__PURE__*/ jsxs(TooltipRoot, { children: [ /*#__PURE__*/ jsx(TooltipTrigger, { asChild: true, children: /*#__PURE__*/ jsx(InformationCircleIcon, { className: "text-nextadmin-content-default dark:text-dark-nextadmin-content-default h-4 w-4" }) }), /*#__PURE__*/ jsx(TooltipPortal, { children: /*#__PURE__*/ jsx(TooltipContent, { side: "right", className: "px-2 py-1", sideOffset: 4, children: tooltip }) }) ] }) }) ] }), children, description, errors, help ] }); }, ObjectFieldTemplate: (props)=>{ const styleForm = modelOptions?.edit?.styles?._form; return /*#__PURE__*/ jsx("div", { className: twMerge("grid gap-4", styleForm), children: props.properties.map((element)=>element.content) }); }, BaseInputTemplate: ({ onChange, options, onChangeOverride, uiSchema, readonly, formContext, autofocus, rawErrors, schema, registry, hideError, hideLabel, ...props })=>{ const { setFieldDirty } = useFormState(); const onTextChange = ({ target: { value: val } })=>{ setFieldDirty(props.name); onChange("" === val ? options.emptyValue || "" : val); }; const customInput = customInputs?.[props.name]; if (customInput) return /*#__PURE__*/ cloneElement(customInput, { value: props.value, onChange: onChangeOverride || onTextChange, readonly, rawErrors, name: props.name, required: props.required, disabled: props.disabled, mode: edit ? "edit" : "create", item: data }); if (schema?.format === "json") return /*#__PURE__*/ jsx(JsonField_mjs_default, { onChange: onChangeOverride || onTextChange, readonly: readonly, rawErrors: rawErrors, ...props }); if (schema?.format?.startsWith("richtext-")) return /*#__PURE__*/ jsx(Suspense, { fallback: /*#__PURE__*/ jsx("div", { className: "h-48 animate-pulse rounded bg-gray-500" }), children: /*#__PURE__*/ jsx(RichTextField, { onChange: onChangeOverride || onTextChange, readonly: readonly, rawErrors: rawErrors, name: props.name, value: props.value, schema: schema, disabled: props.disabled, required: props.required }) }); if ([ "time", "time-second" ].includes(schema?.format)) return /*#__PURE__*/ jsx(BaseInput_mjs_default, { type: "time", onChange: onChangeOverride || onTextChange, ...props, value: props.value.split(":").slice(0, schema?.format === "time-second" ? 3 : 2).join(":") ?? "", className: external_clsx_default({ "ring-red-600 dark:ring-red-400": rawErrors }), step: schema?.format === "time-second" ? 1 : void 0 }); return /*#__PURE__*/ jsx(BaseInput_mjs_default, { onChange: onChangeOverride || onTextChange, ...props, value: props.value ?? "", className: external_clsx_default({ "ring-red-600 dark:ring-red-400": rawErrors }) }); }, ErrorListTemplate: ()=>null, FieldErrorTemplate: ({ errors })=>errors ? /*#__PURE__*/ jsx("div", { className: "mt-1 text-sm text-red-600 dark:text-red-400", children: errors.map((error, idx)=>{ if ("string" == typeof error) return /*#__PURE__*/ jsx(external_react_default.Fragment, { children: t(error) }, idx); return /*#__PURE__*/ jsx(external_react_default.Fragment, { children: error }, idx); }) }) : null, DescriptionFieldTemplate: ({ description, schema })=>description && "null" !== schema.type ? /*#__PURE__*/ jsx("span", { className: "text-nextadmin-content-emphasis dark:text-dark-nextadmin-content-emphasis text-sm", children: description }) : null, ButtonTemplates: { SubmitButton: submitButton } }; const CustomForm = useMemo(()=>/*#__PURE__*/ forwardRef((props, ref)=>{ const { dirtyFields } = useFormState(); const { formData } = useFormData(); return /*#__PURE__*/ jsx("form", { ...props, ref: ref, onSubmit: (e)=>{ e.preventDefault(); const formValues = new FormData(e.target); const data = new FormData(); dirtyFields.forEach((field)=>{ const schemaProperties = schema.properties[field]; const isFieldArrayOfFiles = schemaProperties?.type === "array" && isFileUploadFormat(schemaProperties.format ?? ""); if (isFieldArrayOfFiles) { const files = formValues.getAll(field).filter((file)=>"string" == typeof file || file instanceof File && !!file.name); const values = formData[field]; values.forEach((val)=>{ data.append(field, val); }); if (files.length > 0) files.forEach((file)=>{ data.append(field, file); }); else data.append(field, new File([], "")); return; } data.append(field, formValues.get(field)); }); const submitter = e.nativeEvent.submitter; data.append(submitter.name, submitter.value); onSubmit(data); } }); }), [ onSubmit, schema ]); const RjsfFormComponent = useMemo(()=>/*#__PURE__*/ forwardRef((_props, ref)=>{ const { formData } = useFormData(); return /*#__PURE__*/ jsx(core_default, { tagName: CustomForm, idPrefix: "", idSeparator: "", ...schemas, onChange: (e)=>{ setFormData(e.formData); }, formData: formData, validator: validator_ajv8_default, extraErrors: extraErrors, fields: fields, disabled: allDisabled, formContext: { isPending, schema }, templates: templates, widgets: widgets, ref: ref, className: "relative" }); }), [ submitButton ]); return /*#__PURE__*/ jsx("div", { className: "relative h-full", children: /*#__PURE__*/ jsxs("div", { className: "bg-nextadmin-background-default dark:bg-dark-nextadmin-background-default max-w-full p-4 align-middle sm:p-8", children: [ /*#__PURE__*/ jsx(Message, { className: "-mt-2 mb-2 sm:-mt-4 sm:mb-4" }), /*#__PURE__*/ jsx("div", { className: "bg-nextadmin-background-default dark:bg-dark-nextadmin-background-emphasis border-nextadmin-border-default dark:border-dark-nextadmin-border-default max-w-screen-md rounded-lg border p-4 sm:p-8", children: /*#__PURE__*/ jsx(RjsfFormComponent, { ref: formRef }) }) ] }) }); }; const FormWrapper = ({ clientActionsComponents, relationshipsRawData, ...props })=>/*#__PURE__*/ jsxs(ClientActionDialogContext_mjs_default, { componentsMap: clientActionsComponents, children: [ /*#__PURE__*/ jsx(external_FormHeader_mjs_default, { ...props }), /*#__PURE__*/ jsx(FormDataContext_mjs_default, { data: props.data, relationshipsRawData: relationshipsRawData, children: /*#__PURE__*/ jsx(FormStateContext_mjs_default, { children: /*#__PURE__*/ jsx(Form, { ...props }) }) }) ] }); const components_Form = FormWrapper; export { components_Form as default };