From cdebaac5a51d5718fcb7ab997c3bf262d9745280 Mon Sep 17 00:00:00 2001 From: Hugo FOYART <11079152+foyarash@users.noreply.github.com> Date: Fri, 13 Sep 2024 13:09:48 +0200 Subject: [PATCH] feat: add client actions (#417) --- .changeset/perfect-toys-bathe.md | 5 + apps/docs/pages/docs/api/components.mdx | 80 ++++++++ .../pages/docs/api/model-configuration.mdx | 181 ++++++++++++++++-- .../components/UserDetailsDialogContent.tsx | 30 +++ apps/example/messages/en.json | 3 + apps/example/messages/fr.json | 3 + apps/example/options.tsx | 7 + packages/next-admin/package.json | 5 + packages/next-admin/src/appHandler.ts | 16 +- .../src/components/ActionDropdownItem.tsx | 22 ++- .../src/components/ActionsDropdown.tsx | 4 +- .../src/components/ClientActionDialog.tsx | 97 ++++++++++ packages/next-admin/src/components/List.tsx | 32 ++-- .../next-admin/src/components/ListHeader.tsx | 7 +- .../next-admin/src/components/NextAdmin.tsx | 7 +- .../advancedSearch/AdvancedSearchModal.tsx | 7 +- packages/next-admin/src/components/index.ts | 8 + .../src/components/radix/Dialog.tsx | 5 +- .../src/components/radix/Dropdown.tsx | 28 ++- .../src/context/ClientDialogContext.tsx | 56 ++++++ packages/next-admin/src/hooks/useAction.ts | 6 +- packages/next-admin/src/index.ts | 1 - packages/next-admin/src/pageHandler.ts | 12 +- packages/next-admin/src/types.ts | 35 +++- packages/next-admin/src/utils/options.ts | 21 ++ packages/next-admin/src/utils/props.ts | 8 +- packages/tsconfig/base.json | 2 +- 27 files changed, 624 insertions(+), 64 deletions(-) create mode 100644 .changeset/perfect-toys-bathe.md create mode 100644 apps/docs/pages/docs/api/components.mdx create mode 100644 apps/example/components/UserDetailsDialogContent.tsx create mode 100644 packages/next-admin/src/components/ClientActionDialog.tsx create mode 100644 packages/next-admin/src/components/index.ts create mode 100644 packages/next-admin/src/context/ClientDialogContext.tsx diff --git a/.changeset/perfect-toys-bathe.md b/.changeset/perfect-toys-bathe.md new file mode 100644 index 00000000..7da1c2da --- /dev/null +++ b/.changeset/perfect-toys-bathe.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": minor +--- + +feat: add client actions (#401) diff --git a/apps/docs/pages/docs/api/components.mdx b/apps/docs/pages/docs/api/components.mdx new file mode 100644 index 00000000..ab7b796f --- /dev/null +++ b/apps/docs/pages/docs/api/components.mdx @@ -0,0 +1,80 @@ +import OptionsTable from "../../../components/OptionsTable"; + +# Components + +Next-Admin exports a set of UI components which are, for most of those, extending [Radix UI primitives](https://www.radix-ui.com/primitives). These components are available through the `@premieroctet/next-admin/components` import. + +## `Button` + +The button element accepts all the base `button` tag attributes and extends it with the following: + + + A boolean to render a{" "} + + Radix Slot component + + + ), + }, + { + name: "loading", + description: "A boolean to render a spinner", + }, + ]} +/> + +## `BaseInput` + +The input component rendered in the Form component. It accepts all the base `input` tag attributes. + +## `Switch` + +The [Radix Switch](https://www.radix-ui.com/primitives/docs/components/switch) component. + +## `Select` + +The [Radix Select](https://www.radix-ui.com/primitives/docs/components/select) component. + +## `Checkbox` + +An implementation of the [Radix Checkbox](https://www.radix-ui.com/primitives/docs/components/checkbox) component. It accepts all the props of the [Checkbox Root](https://www.radix-ui.com/primitives/docs/components/switch#root) component, and the following: + + + +## `Dropdown` + +The [Radix Dropdown](https://www.radix-ui.com/primitives/docs/components/dropdown) component. + +## `Table` + +The [Radix Table](https://www.radix-ui.com/primitives/docs/components/table) component. + +## `Tooltip` + +The [Radix Tooltip](https://www.radix-ui.com/primitives/docs/components/tooltip) component. diff --git a/apps/docs/pages/docs/api/model-configuration.mdx b/apps/docs/pages/docs/api/model-configuration.mdx index 10de5ddb..aaeb3b20 100644 --- a/apps/docs/pages/docs/api/model-configuration.mdx +++ b/apps/docs/pages/docs/api/model-configuration.mdx @@ -114,9 +114,8 @@ By default, if no models are defined, they will all be displayed in the admin. I description: ( <> {" "} - an array of actions (see - actions property - ) + an array of actions (see{" "} + actions property) ), }, @@ -200,9 +199,8 @@ This property determines how your data is displayed in the [list View](/docs/glo description: ( <> {" "} - define a set of Prisma filters that user can choose in list (see - filters - ) + define a set of Prisma filters that user can choose in list (see{" "} + filters) ), }, @@ -315,6 +313,7 @@ The `exports` property is available in the `list` property. It's an object or an This property determines how your data is displayed in the [edit view](/docs/glossary#edit-view) {" "} + {" "} - an array of fields that are displayed in the form. It can also be an object - that will be displayed in the form of a notice (see - notice - ) + an array of fields that are displayed in the form. It can also be an + object that will be displayed in the form of a notice (see{" "} + notice) ), defaultValue: "all scalar fields are displayed", @@ -589,13 +587,39 @@ The `actions` property is an array of objects that allows you to define a set of type: "String", description: "mandatory, action's unique identifier", }, + { + name: "type", + type: "String", + description: + "optional action type for client side actions, possible value is 'dialog'. By default action with no type is executed on the server", + }, + { + name: "component", + type: "ReactElement", + description: ( + <> + a React component that will be displayed in a dialog when the action + is triggered. Its mandatory if the action type is specified + + ), + }, + { + name: "className", + type: "string", + description: ( + <> + class name applied to the dialog displayed when the action type is set + to 'dialog'. + + ), + }, { name: "action", type: "Function", description: ( <> an async function that will be triggered when selecting the action in - the dropdown. For App Router, it must be defined as a server action + the dropdown. Its mandatory if the action type is not specified ), }, @@ -732,9 +756,11 @@ Represents the props that are passed to the custom input component. description: ( <> {" "} - a function taking a + a function taking a{" "} + ChangeEvent - as a parameter + {" "} + as a parameter ), }, @@ -775,3 +801,132 @@ The `HookError` is an error that can be thrown in the `beforeDb` hook of the `ed }, ]} /> + +## ClientActionDialogContentProps + +Represents the props that are passed to the custom dialog component. + +", + description: ( + <> + A record of row's properties with{" "} + ListDataFieldValue as value + + ), + }, + { + name: "onClose", + type: "Function", + description: "a function to close the dialog", + }, + ]} +/> + +## ListDataFieldValue + +Represents a formatted value used by Next-Admin. It will have different shapes depending of the data type. + +It will always have the following : + + + +### Scalar + + + +### Count + + + +### Link + + + +### Date + + diff --git a/apps/example/components/UserDetailsDialogContent.tsx b/apps/example/components/UserDetailsDialogContent.tsx new file mode 100644 index 00000000..e6ac8df0 --- /dev/null +++ b/apps/example/components/UserDetailsDialogContent.tsx @@ -0,0 +1,30 @@ +"use client"; +import { ClientActionDialogContentProps } from "@premieroctet/next-admin"; +import { Button } from "@premieroctet/next-admin/components"; + +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} +

+
+
+ +
+
+ ); +}; + +export default UserDetailsDialog; diff --git a/apps/example/messages/en.json b/apps/example/messages/en.json index 087f8d5c..7f0b04f2 100644 --- a/apps/example/messages/en.json +++ b/apps/example/messages/en.json @@ -6,6 +6,9 @@ "title": "Send email", "success": "Email sent successfully", "error": "Error while sending email" + }, + "details": { + "title": "User details" } } }, diff --git a/apps/example/messages/fr.json b/apps/example/messages/fr.json index 07ea3841..dbee3fe3 100644 --- a/apps/example/messages/fr.json +++ b/apps/example/messages/fr.json @@ -110,6 +110,9 @@ "title": "Envoyer un email", "success": "Email envoyé avec succès", "error": "Erreur lors de l'envoi de l'email" + }, + "details": { + "title": "Détails de l'utilisateur" } }, "label": "Action", diff --git a/apps/example/options.tsx b/apps/example/options.tsx index 1c1080a1..2e74f4f8 100644 --- a/apps/example/options.tsx +++ b/apps/example/options.tsx @@ -1,6 +1,7 @@ import { NextAdminOptions } from "@premieroctet/next-admin"; import DatePicker from "./components/DatePicker"; import PasswordInput from "./components/PasswordInput"; +import UserDetailsDialog from "@/components/UserDetailsDialogContent"; export const options: NextAdminOptions = { title: "⚡️ My Admin", @@ -162,6 +163,12 @@ export const options: NextAdminOptions = { successMessage: "actions.user.email.success", errorMessage: "actions.user.email.error", }, + { + type: "dialog", + id: "user-details", + title: "actions.user.details.title", + component: , + }, ], }, Post: { diff --git a/packages/next-admin/package.json b/packages/next-admin/package.json index facd50c3..dff2bb7d 100644 --- a/packages/next-admin/package.json +++ b/packages/next-admin/package.json @@ -66,6 +66,11 @@ "import": "./dist/preset.js", "types": "./dist/preset.d.ts", "require": "./dist/preset.js" + }, + "./components": { + "import": "./dist/components/index.js", + "types": "./dist/components/index.d.ts", + "require": "./dist/components/index.js" } }, "peerDependencies": { diff --git a/packages/next-admin/src/appHandler.ts b/packages/next-admin/src/appHandler.ts index cbdc3821..6f932f14 100644 --- a/packages/next-admin/src/appHandler.ts +++ b/packages/next-admin/src/appHandler.ts @@ -2,7 +2,12 @@ import { createEdgeRouter } from "next-connect"; import { NextRequest, NextResponse } from "next/server"; import { handleOptionsSearch } from "./handlers/options"; import { deleteResource, submitResource } from "./handlers/resources"; -import { CreateAppHandlerParams, Permission, RequestContext } from "./types"; +import { + CreateAppHandlerParams, + Permission, + RequestContext, + ServerAction, +} from "./types"; import { hasPermission } from "./utils/permissions"; import { formatId, @@ -63,10 +68,17 @@ export const createHandler =

({ ); } + if ("type" in modelAction && modelAction.type === "dialog") { + return NextResponse.json( + { error: "Action not found" }, + { status: 404 } + ); + } + const body = await req.json(); try { - await modelAction.action(body as string[] | number[]); + await (modelAction as ServerAction).action(body as string[] | number[]); return NextResponse.json({ ok: true }); } catch (e) { diff --git a/packages/next-admin/src/components/ActionDropdownItem.tsx b/packages/next-admin/src/components/ActionDropdownItem.tsx index 2b34987e..88d07281 100644 --- a/packages/next-admin/src/components/ActionDropdownItem.tsx +++ b/packages/next-admin/src/components/ActionDropdownItem.tsx @@ -1,18 +1,23 @@ import clsx from "clsx"; +import { useClientDialog } from "../context/ClientDialogContext"; +import { useI18n } from "../context/I18nContext"; import { useAction } from "../hooks/useAction"; import { ModelAction, ModelName } from "../types"; import { DropdownItem } from "./radix/Dropdown"; -import { useI18n } from "../context/I18nContext"; type Props = { - action: ModelAction | Omit; + action: ModelAction | Omit, "action">; resource: ModelName; resourceIds: string[] | number[]; + data?: any; }; -const ActionDropdownItem = ({ action, resource, resourceIds }: Props) => { +const ActionDropdownItem = ({ action, resource, resourceIds, data }: Props) => { const { t } = useI18n(); const { runAction } = useAction(resource, resourceIds); + const isClientAction = + "component" in action && "type" in action && action.type === "dialog"; + const { open } = useClientDialog(); return ( { })} onClick={(evt) => { evt.stopPropagation(); - runAction(action); + if (isClientAction) { + open({ + actionId: action.id, + data: data, + resource: resource, + resourceId: resourceIds[0], + }); + } else { + runAction(action); + } }} > {t(action.title)} diff --git a/packages/next-admin/src/components/ActionsDropdown.tsx b/packages/next-admin/src/components/ActionsDropdown.tsx index 1111ea88..14e0c0c3 100644 --- a/packages/next-admin/src/components/ActionsDropdown.tsx +++ b/packages/next-admin/src/components/ActionsDropdown.tsx @@ -15,7 +15,9 @@ import { } from "./radix/Dropdown"; type Props = { - actions: Array>; + actions: Array< + ModelAction | Omit, "action"> + >; selectedIds: string[] | number[]; resource: ModelName; selectedCount?: number; diff --git a/packages/next-admin/src/components/ClientActionDialog.tsx b/packages/next-admin/src/components/ClientActionDialog.tsx new file mode 100644 index 00000000..c22963d2 --- /dev/null +++ b/packages/next-admin/src/components/ClientActionDialog.tsx @@ -0,0 +1,97 @@ +import { Transition, TransitionChild } from "@headlessui/react"; +import { cloneElement, Fragment } from "react"; +import clsx from "clsx"; +import { useClientDialog } from "../context/ClientDialogContext"; +import { useConfig } from "../context/ConfigContext"; +import { + AdminComponentProps, + ModelAction, + ModelName, + ServerAction, +} from "../types"; +import { + DialogContent, + DialogOverlay, + DialogPortal, + DialogRoot, +} from "./radix/Dialog"; + +type Props = { + actionsMap: NonNullable; +}; + +const ClientActionDialog = ({ actionsMap }: Props) => { + const { data, isOpen, onClose, clearData } = useClientDialog(); + const { options } = useConfig(); + + const action = data?.resource + ? (options?.model?.[data.resource]?.actions?.find( + (action: ModelAction) => + action.id === data?.actionId && + "type" in action && + action.type === "dialog" + ) as Exclude, ServerAction>) + : null; + + if (isOpen && !action) { + throw new Error("Action not found"); + } + + return ( + { + if (!open) { + onClose(); + } + }} + modal + > + + + + + + + + {action && + actionsMap[action.id] && + data && + cloneElement(actionsMap[action.id], { + data: data.data, + resource: data.resource, + resourceId: data.resourceId, + onClose, + })} + + + + + + ); +}; + +export default ClientActionDialog; diff --git a/packages/next-admin/src/components/List.tsx b/packages/next-admin/src/components/List.tsx index fd861c30..bbb94e8c 100644 --- a/packages/next-admin/src/components/List.tsx +++ b/packages/next-admin/src/components/List.tsx @@ -3,6 +3,7 @@ import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; import { ColumnDef, RowSelectionState } from "@tanstack/react-table"; import debounce from "lodash/debounce"; import { ChangeEvent, useEffect, useState, useTransition } from "react"; +import clsx from "clsx"; import { ITEMS_PER_PAGE } from "../config"; import { useConfig } from "../context/ConfigContext"; import { useI18n } from "../context/I18nContext"; @@ -41,6 +42,8 @@ import { SelectValue, } from "./radix/Select"; import ActionDropdownItem from "./ActionDropdownItem"; +import ClientDialogProvider from "../context/ClientDialogContext"; +import ClientActionDialog from "./ClientActionDialog"; export type ListProps = { resource: ModelName; @@ -51,6 +54,7 @@ export type ListProps = { actions?: AdminComponentProps["actions"]; icon?: ModelIcon; schema: Schema; + actionsMap: NonNullable; }; function List({ @@ -62,6 +66,7 @@ function List({ title, icon, schema, + actionsMap, }: ListProps) { const { router, query } = useRouterInternal(); const [isPending, startTransition] = useTransition(); @@ -169,20 +174,20 @@ function List({ action={action} resourceIds={[row.original[idProperty].value as string]} resource={resource} + data={row.original} /> ); })} - - + { + evt.stopPropagation(); + deleteItems([row.original[idProperty].value as string]); + }} + > + {t("list.row.actions.delete.label")} @@ -210,7 +215,7 @@ function List({ }; return ( - <> +

- + + ); } diff --git a/packages/next-admin/src/components/ListHeader.tsx b/packages/next-admin/src/components/ListHeader.tsx index e9bacc66..c1cc0c2b 100644 --- a/packages/next-admin/src/components/ListHeader.tsx +++ b/packages/next-admin/src/components/ListHeader.tsx @@ -70,7 +70,7 @@ export default function ListHeader({ const selectedRowsCount = Object.keys(selectedRows).length; const actions = useMemo(() => { - const defaultActions: ModelAction[] = canDelete + const defaultActions: ModelAction[] = canDelete ? [ { id: SPECIFIC_IDS_TO_RUN_ACTION.DELETE, @@ -139,7 +139,10 @@ export default function ListHeader({ {Boolean(selectedRowsCount) && !!actions.length && ( !("type" in action && action.type === "dialog") + )} resource={resource} selectedIds={getSelectedRowsIds()} selectedCount={selectedRowsCount} diff --git a/packages/next-admin/src/components/NextAdmin.tsx b/packages/next-admin/src/components/NextAdmin.tsx index 27c66986..0924644d 100644 --- a/packages/next-admin/src/components/NextAdmin.tsx +++ b/packages/next-admin/src/components/NextAdmin.tsx @@ -1,7 +1,7 @@ import dynamic from "next/dynamic"; import { AdminComponentProps, CustomUIProps } from "../types"; import { getSchemaForResource } from "../utils/jsonSchema"; -import { getCustomInputs } from "../utils/options"; +import { getClientActionDialogs, getCustomInputs } from "../utils/options"; import Dashboard from "./Dashboard"; import Form from "./Form"; import List from "./List"; @@ -37,6 +37,7 @@ export function NextAdmin({ resourcesIcons, user, externalLinks, + actionsMap: actionsMapProp, }: AdminComponentProps & CustomUIProps) { if (!isAppDir && !options) { throw new Error( @@ -55,6 +56,9 @@ export function NextAdmin({ const renderMainComponent = () => { if (Array.isArray(data) && resource && typeof total != "undefined") { + const actionsMap = isAppDir + ? actionsMapProp + : getClientActionDialogs(resource!, options!); return ( ); } diff --git a/packages/next-admin/src/components/advancedSearch/AdvancedSearchModal.tsx b/packages/next-admin/src/components/advancedSearch/AdvancedSearchModal.tsx index 79c78057..3dc5ed52 100644 --- a/packages/next-admin/src/components/advancedSearch/AdvancedSearchModal.tsx +++ b/packages/next-admin/src/components/advancedSearch/AdvancedSearchModal.tsx @@ -122,10 +122,7 @@ const AdvancedSearchModal = ({ isOpen, onClose, resource, schema }: Props) => { leaveTo="opacity-0" leave="transition-opacity ease-in-out duration-300" > - + { >
{t("search.advanced.title")} diff --git a/packages/next-admin/src/components/index.ts b/packages/next-admin/src/components/index.ts new file mode 100644 index 00000000..5826d1d7 --- /dev/null +++ b/packages/next-admin/src/components/index.ts @@ -0,0 +1,8 @@ +export { Button, type ButtonProps, buttonVariants } from "./radix/Button"; +export { default as BaseInput } from "./inputs/BaseInput"; +export * from "./radix/Switch"; +export * from "./radix/Select"; +export { default as Checkbox } from "./radix/Checkbox"; +export * from "./radix/Dropdown"; +export * from "./radix/Table"; +export * from "./radix/Tooltip"; diff --git a/packages/next-admin/src/components/radix/Dialog.tsx b/packages/next-admin/src/components/radix/Dialog.tsx index 1bcfa88a..42d253e7 100644 --- a/packages/next-admin/src/components/radix/Dialog.tsx +++ b/packages/next-admin/src/components/radix/Dialog.tsx @@ -14,7 +14,10 @@ export const DialogOverlay = forwardRef< return ( (({ className, ...props }, ref) => { return ( (({ className, ...props }, ref) => { return ( (({ className, ...props }, ref) => { return ( @@ -89,7 +96,12 @@ export const DropdownSeparator = forwardRef< >(({ className, ...props }, ref) => { return ( diff --git a/packages/next-admin/src/context/ClientDialogContext.tsx b/packages/next-admin/src/context/ClientDialogContext.tsx new file mode 100644 index 00000000..ff475993 --- /dev/null +++ b/packages/next-admin/src/context/ClientDialogContext.tsx @@ -0,0 +1,56 @@ +import { createContext, PropsWithChildren, useContext, useState } from "react"; +import { + AdminComponentProps, + ListDataFieldValue, + Model, + ModelName, +} from "../types"; + +type ClientDialogData = { + resource: ModelName; + resourceId: string | number; + actionId: string; + data: Record, ListDataFieldValue>; +}; + +type ClientDialogContextType = { + isOpen: boolean; + onClose: () => void; + open: (data: ClientDialogData) => void; + data: ClientDialogData | null; + clearData: () => void; +}; + +const ClientDialogContext = createContext( + {} as ClientDialogContextType +); + +const ClientDialogProvider = ({ children }: PropsWithChildren) => { + const [isOpen, setIsOpen] = useState(false); + const [dialogData, setDialogData] = useState(null); + + const onClose = () => { + setIsOpen(false); + }; + + const open = (data: ClientDialogData) => { + setIsOpen(true); + setDialogData(data); + }; + + const clearData = () => { + setDialogData(null); + }; + + return ( + + {children} + + ); +}; + +export const useClientDialog = () => useContext(ClientDialogContext); + +export default ClientDialogProvider; diff --git a/packages/next-admin/src/hooks/useAction.ts b/packages/next-admin/src/hooks/useAction.ts index 5bb6fc52..821479ae 100644 --- a/packages/next-admin/src/hooks/useAction.ts +++ b/packages/next-admin/src/hooks/useAction.ts @@ -1,4 +1,4 @@ -import { ModelAction, ModelName } from "../types"; +import { ClientAction, ModelAction, ModelName } from "../types"; import { useI18n } from "../context/I18nContext"; import { useConfig } from "../context/ConfigContext"; import { useMessage } from "../context/MessageContext"; @@ -13,7 +13,9 @@ export const useAction = (resource: ModelName, ids: string[] | number[]) => { const { showMessage } = useMessage(); const runAction = async ( - modelAction: ModelAction | Omit + modelAction: + | Exclude, ClientAction> + | Omit, ClientAction>, "action"> ) => { try { if ( diff --git a/packages/next-admin/src/index.ts b/packages/next-admin/src/index.ts index bdefe863..e7b6f23c 100644 --- a/packages/next-admin/src/index.ts +++ b/packages/next-admin/src/index.ts @@ -1,5 +1,4 @@ export * from "./components/MainLayout"; export * from "./components/NextAdmin"; -export * from "./components/inputs/BaseInput"; export * from "./types"; export * from "./exceptions/HookError"; diff --git a/packages/next-admin/src/pageHandler.ts b/packages/next-admin/src/pageHandler.ts index 463d0501..5b00fc62 100644 --- a/packages/next-admin/src/pageHandler.ts +++ b/packages/next-admin/src/pageHandler.ts @@ -3,7 +3,7 @@ import { NextApiRequest, NextApiResponse } from "next"; import { NextHandler, createRouter } from "next-connect"; import { handleOptionsSearch } from "./handlers/options"; import { deleteResource, submitResource } from "./handlers/resources"; -import { NextAdminOptions, Permission } from "./types"; +import { NextAdminOptions, Permission, ServerAction } from "./types"; import { hasPermission } from "./utils/permissions"; import { formatId, @@ -88,6 +88,10 @@ export const createHandler =

({ return res.status(404).json({ error: "Action not found" }); } + if ("type" in modelAction && modelAction.type === "dialog") { + return res.status(404).json({ error: "Action not found" }); + } + let body; try { @@ -97,7 +101,7 @@ export const createHandler =

({ } try { - await modelAction.action(body); + await (modelAction as ServerAction).action(body); return res.json({ ok: true }); } catch (e) { @@ -160,7 +164,9 @@ export const createHandler =

({ }); } - response = (await editOptions?.hooks?.afterDb?.(response, mode, req)) ?? response; + response = + (await editOptions?.hooks?.afterDb?.(response, mode, req)) ?? + response; return res.status(id ? 200 : 201).json(response); } catch (e) { diff --git a/packages/next-admin/src/types.ts b/packages/next-admin/src/types.ts index 21336159..a785e0de 100644 --- a/packages/next-admin/src/types.ts +++ b/packages/next-admin/src/types.ts @@ -532,15 +532,30 @@ type CustomFieldsType = { export type ActionStyle = "default" | "destructive"; -export type ModelAction = { - title: string; - id: string; +export type ServerAction = { action: (ids: string[] | number[]) => Promise; - style?: ActionStyle; successMessage?: string; errorMessage?: string; }; +export type ClientAction = { + type: "dialog"; + component: React.ReactElement>; + /** + * Class name to apply to the dialog content + */ + className?: string; +}; + +export type ModelAction = ( + | ServerAction + | ClientAction +) & { + title: string; + id: string; + style?: ActionStyle; +}; + export type ModelIcon = keyof typeof OutlineIcons; export enum Permission { @@ -574,7 +589,7 @@ export type ModelOptions = { * an object containing the aliases of the model fields as keys, and the field name. */ aliases?: Partial, string>> & { [key: string]: string }; - actions?: ModelAction[]; + actions?: ModelAction[]; /** * the outline HeroIcon name displayed in the sidebar and pages title * @type ModelIcon @@ -801,7 +816,8 @@ export type AdminComponentProps = { */ pageComponent?: React.ComponentType; customPages?: Array<{ title: string; path: string; icon?: ModelIcon }>; - actions?: Omit[]; + actions?: Omit, "action">[]; + actionsMap?: Record; translations?: Translations; /** * Global admin title @@ -1050,3 +1066,10 @@ export type FormProps = { icon?: ModelIcon; resourcesIdProperty: Record; }; + +export type ClientActionDialogContentProps = Partial<{ + resource: ModelName; + resourceId: string | number; + data: Record, ListDataFieldValue>; + onClose?: () => void; +}>; diff --git a/packages/next-admin/src/utils/options.ts b/packages/next-admin/src/utils/options.ts index 58a7b8e3..fe58371d 100644 --- a/packages/next-admin/src/utils/options.ts +++ b/packages/next-admin/src/utils/options.ts @@ -27,3 +27,24 @@ export const getCustomInputs = ( return acc; }, {}); }; + +export const getClientActionDialogs = ( + model: ModelName, + options?: NextAdminOptions +) => { + const actions = options?.model?.[model]?.actions; + const actionsMap: Record = {}; + + actions?.forEach((action) => { + if ( + "component" in action && + "type" in action && + action.type === "dialog" && + action.component + ) { + actionsMap[action.id] = action.component; + } + }); + + return actionsMap; +}; diff --git a/packages/next-admin/src/utils/props.ts b/packages/next-admin/src/utils/props.ts index f401b69e..61c56c56 100644 --- a/packages/next-admin/src/utils/props.ts +++ b/packages/next-admin/src/utils/props.ts @@ -10,7 +10,7 @@ import { ModelName, NextAdminOptions, } from "../types"; -import { getCustomInputs } from "./options"; +import { getClientActionDialogs, getCustomInputs } from "./options"; import { getDataItem, getMappedDataList } from "./prisma"; import { applyVisiblePropertiesInSchema, @@ -97,6 +97,7 @@ export async function getPropsFromParams({ // We don't need to pass the action function to the component const actions = options?.model?.[resource]?.actions?.map((action) => { + // @ts-expect-error const { action: _, ...actionRest } = action; return actionRest; }); @@ -133,6 +134,10 @@ export async function getPropsFromParams({ appDir: isAppDir, }); + const actionsMap = isAppDir + ? getClientActionDialogs(resource, options) + : undefined; + return { ...defaultProps, resource, @@ -141,6 +146,7 @@ export async function getPropsFromParams({ error: error ?? (searchParams?.error as string), schema, actions: isAppDir ? actions : undefined, + actionsMap, }; } case Page.EDIT: { diff --git a/packages/tsconfig/base.json b/packages/tsconfig/base.json index d72a9f3a..668ffd31 100644 --- a/packages/tsconfig/base.json +++ b/packages/tsconfig/base.json @@ -8,7 +8,7 @@ "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "inlineSources": false, - "isolatedModules": true, + "isolatedModules": false, "moduleResolution": "node", "noUnusedLocals": false, "noUnusedParameters": false,