From 56ea03b1896eba1cd5ac5a456e2d1b3024182cfb Mon Sep 17 00:00:00 2001 From: Hugo FOYART <11079152+foyarash@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:02:36 +0200 Subject: [PATCH] feat: add depth selection for actions (#470) --- .changeset/dry-tips-thank.md | 5 +++ .../pages/docs/api/model-configuration.mdx | 6 +++ apps/example/options.tsx | 1 + packages/next-admin/src/appHandler.ts | 16 ++++++- .../src/components/ClientActionDialog.tsx | 13 ++++-- packages/next-admin/src/pageHandler.ts | 13 +++++- packages/next-admin/src/types.ts | 6 +++ packages/next-admin/src/utils/prisma.ts | 44 +++++++++++++++---- packages/next-admin/src/utils/server.ts | 4 ++ 9 files changed, 94 insertions(+), 14 deletions(-) create mode 100644 .changeset/dry-tips-thank.md diff --git a/.changeset/dry-tips-thank.md b/.changeset/dry-tips-thank.md new file mode 100644 index 00000000..a5d34e75 --- /dev/null +++ b/.changeset/dry-tips-thank.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +feat: add depth selection for actions (#443) diff --git a/apps/docs/pages/docs/api/model-configuration.mdx b/apps/docs/pages/docs/api/model-configuration.mdx index 257a1296..38707bea 100644 --- a/apps/docs/pages/docs/api/model-configuration.mdx +++ b/apps/docs/pages/docs/api/model-configuration.mdx @@ -652,6 +652,12 @@ The `actions` property is an array of objects that allows you to define a set of description: "a message that will be displayed when the action fails if action doesn't return a message object or throw an error with a message", }, + { + name: "depth", + type: "Number", + description: + "a number that defines the depth of the relations to select in the resource. Use this with caution, a number too high can potentially cause slower queries. Defaults to 2.", + }, ]} /> diff --git a/apps/example/options.tsx b/apps/example/options.tsx index db180c47..0b9025a9 100644 --- a/apps/example/options.tsx +++ b/apps/example/options.tsx @@ -189,6 +189,7 @@ export const options: NextAdminOptions = { id: "user-details", title: "actions.user.details.title", component: , + depth: 3, }, ], }, diff --git a/packages/next-admin/src/appHandler.ts b/packages/next-admin/src/appHandler.ts index 9596a721..6271afb6 100644 --- a/packages/next-admin/src/appHandler.ts +++ b/packages/next-admin/src/appHandler.ts @@ -58,11 +58,25 @@ export const createHandler =

({ ?.split(",") .map((id) => formatId(resource, id)); + const depth = req.nextUrl.searchParams.get("depth"); + if (!ids) { return NextResponse.json({ error: "No ids provided" }, { status: 400 }); } - const data = await getRawData({ prisma, resource, resourceIds: ids }); + if (depth && isNaN(Number(depth))) { + return NextResponse.json( + { error: "Depth should be a number" }, + { status: 400 } + ); + } + + const data = await getRawData({ + prisma, + resource, + resourceIds: ids, + maxDepth: depth ? Number(depth) : undefined, + }); return NextResponse.json(data); }) diff --git a/packages/next-admin/src/components/ClientActionDialog.tsx b/packages/next-admin/src/components/ClientActionDialog.tsx index 49e0fae0..3dcd5f3d 100644 --- a/packages/next-admin/src/components/ClientActionDialog.tsx +++ b/packages/next-admin/src/components/ClientActionDialog.tsx @@ -42,9 +42,16 @@ const ClientActionDialog = ({ useEffect(() => { setIsLoading(true); - fetch( - `${apiBasePath}/${slugify(resource)}/raw?ids=${resourceIds.join(",")}` - ) + const params = new URLSearchParams(); + + params.set("ids", resourceIds.join(",")); + + if (action.depth) { + // Avoid negative depth + params.set("depth", Math.max(1, action.depth).toString()); + } + + fetch(`${apiBasePath}/${slugify(resource)}/raw?${params.toString()}`) .then((res) => res.json()) .then(setData) .finally(() => { diff --git a/packages/next-admin/src/pageHandler.ts b/packages/next-admin/src/pageHandler.ts index bcaef78a..319e305a 100644 --- a/packages/next-admin/src/pageHandler.ts +++ b/packages/next-admin/src/pageHandler.ts @@ -90,7 +90,18 @@ export const createHandler =

({ ids = ids?.split(",").map((id: string) => formatId(resource, id)); } - const data = await getRawData({ prisma, resource, resourceIds: ids }); + const depth = req.query.depth; + + if (depth && isNaN(Number(depth))) { + return res.status(400).json({ error: "Depth should be a number" }); + } + + const data = await getRawData({ + prisma, + resource, + resourceIds: ids, + maxDepth: depth ? Number(depth) : undefined, + }); return res.json(data); }) diff --git a/packages/next-admin/src/types.ts b/packages/next-admin/src/types.ts index 774c17b7..4477c1c5 100644 --- a/packages/next-admin/src/types.ts +++ b/packages/next-admin/src/types.ts @@ -549,6 +549,12 @@ export type BareModelAction = { canExecute?: (item: Model) => boolean; icon?: keyof typeof OutlineIcons; style?: ActionStyle; + /** + * Max depth of the related records to select + * + * @default 2 + */ + depth?: number; }; export type ServerAction = { diff --git a/packages/next-admin/src/utils/prisma.ts b/packages/next-admin/src/utils/prisma.ts index f9ecb028..45b58d15 100644 --- a/packages/next-admin/src/utils/prisma.ts +++ b/packages/next-admin/src/utils/prisma.ts @@ -661,6 +661,38 @@ export const getDataItem = async ({ return data; }; +type DeepIncludeRecord = Record; + +const includeDataByDepth = ( + model: Prisma.DMMF.Model, + currentDepth: number, + maxDepth: number +) => { + const include = model?.fields.reduce((acc, field) => { + if (field.kind === "object") { + /** + * We substract because, if the condition matches, + * we will have all the fields in the related model, which are + * counted in currentDepth + 1 + */ + if (currentDepth < maxDepth - 1) { + acc[field.name] = { + include: includeDataByDepth( + getPrismaModelForResource(field.type as M)!, + currentDepth + 1, + maxDepth + ), + }; + } else { + acc[field.name] = true; + } + } + return acc; + }, {} as DeepIncludeRecord); + + return include; +}; + /** * Get raw data from Prisma (2-deep nested relations) * @param prisma @@ -672,22 +704,16 @@ export const getRawData = async ({ prisma, resource, resourceIds, + maxDepth = 2, }: { prisma: PrismaClient; resource: M; resourceIds: Array; + maxDepth?: number; }): Promise[]> => { const modelDMMF = getPrismaModelForResource(resource); - const include = modelDMMF?.fields.reduce( - (acc, field) => { - if (field.kind === "object") { - acc[field.name] = true; - } - return acc; - }, - {} as Record - ); + const include = includeDataByDepth(modelDMMF!, 1, maxDepth); // @ts-expect-error const data = await prisma[resource].findMany({ diff --git a/packages/next-admin/src/utils/server.ts b/packages/next-admin/src/utils/server.ts index a8e3299a..e39729ec 100644 --- a/packages/next-admin/src/utils/server.ts +++ b/packages/next-admin/src/utils/server.ts @@ -50,10 +50,14 @@ export const getEnableToExecuteActions = async ( actions?: Omit, "action">[] ): Promise => { if (actions?.some((action) => action.canExecute)) { + const maxDepth = Math.max(0, ...actions.map((action) => action.depth ?? 0)); + const data: Model[] = await getRawData({ prisma, resource, resourceIds: ids, + // Apply the default value if its 0 + maxDepth: maxDepth || undefined, }); return actions?.reduce(