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 1/4] 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( From 3225788bd475ff27a537f1eefc8e4da4bbaa14ff Mon Sep 17 00:00:00 2001 From: Regourd Colin Date: Mon, 7 Oct 2024 12:03:28 +0200 Subject: [PATCH 2/4] Fix image (#472) --- .changeset/witty-snakes-shout.md | 5 +++++ apps/example/options.tsx | 2 +- .../src/components/inputs/FileWidget.tsx | 18 ++++++++++++++---- packages/next-admin/src/utils/server.ts | 7 ++++--- 4 files changed, 24 insertions(+), 8 deletions(-) create mode 100644 .changeset/witty-snakes-shout.md diff --git a/.changeset/witty-snakes-shout.md b/.changeset/witty-snakes-shout.md new file mode 100644 index 00000000..218d6418 --- /dev/null +++ b/.changeset/witty-snakes-shout.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Fix image (get async) diff --git a/apps/example/options.tsx b/apps/example/options.tsx index 0b9025a9..3c281835 100644 --- a/apps/example/options.tsx +++ b/apps/example/options.tsx @@ -1,5 +1,5 @@ -import UserDetailsDialog from "@/components/UserDetailsDialogContent"; import AddTagDialog from "@/components/PostAddTagDialogContent"; +import UserDetailsDialog from "@/components/UserDetailsDialogContent"; import { NextAdminOptions } from "@premieroctet/next-admin"; import DatePicker from "./components/DatePicker"; import PasswordInput from "./components/PasswordInput"; diff --git a/packages/next-admin/src/components/inputs/FileWidget.tsx b/packages/next-admin/src/components/inputs/FileWidget.tsx index 35276853..e0fb30e0 100644 --- a/packages/next-admin/src/components/inputs/FileWidget.tsx +++ b/packages/next-admin/src/components/inputs/FileWidget.tsx @@ -9,12 +9,12 @@ import { ChangeEvent, useEffect, useRef, useState } from "react"; import Loader from "../../assets/icons/Loader"; import { useFormState } from "../../context/FormStateContext"; import { useI18n } from "../../context/I18nContext"; -import { getFilenameFromUrl, isImageType } from "../../utils/file"; +import { getFilenameFromUrl } from "../../utils/file"; const FileWidget = (props: WidgetProps) => { const [file, setFile] = useState(); const [errors, setErrors] = useState(props.rawErrors); - const [fileIsImage, setFileIsImage] = useState(false); + const [fileIsImage, setFileIsImage] = useState(true); const [filename, setFilename] = useState(null); const [fileUrl, setFileUrl] = useState(props.value); const [isPending, setIsPending] = useState(false); @@ -35,12 +35,22 @@ const FileWidget = (props: WidgetProps) => { useEffect(() => { if (props.value) { setIsPending(true); - setFileUrl(props.value); + + const image = document.createElement("img"); + image.src = props.value as string; + image.onload = () => { + setFileIsImage(true); + setIsPending(false); + }; + image.onerror = (e) => { + console.error(e); + setFileIsImage(false); + setIsPending(false); + }; const filename = getFilenameFromUrl(props.value); if (filename) { setFilename(filename); } - setFileIsImage(isImageType(props.value)); setIsPending(false); } else { setIsPending(false); diff --git a/packages/next-admin/src/utils/server.ts b/packages/next-admin/src/utils/server.ts index e39729ec..182fb51c 100644 --- a/packages/next-admin/src/utils/server.ts +++ b/packages/next-admin/src/utils/server.ts @@ -317,7 +317,8 @@ export const transformData = ( const model = models.find((model) => model.name === modelName); if (!model) return data; - return Object.keys(data).reduce((acc, key) => { + return Object.keys(data).reduce(async (accP, key) => { + const acc = await accP; const field = model.fields?.find((field) => field.name === key); const fieldKind = field?.kind; const get = editOptions?.fields?.[key as Field]?.handler?.get; @@ -326,7 +327,7 @@ export const transformData = ( editOptions?.fields?.[key as Field]?.relationshipSearchField; if (get) { - acc[key] = get(data[key]); + acc[key] = await get(data[key]); } else if (fieldKind === "enum") { const value = data[key]; if (Array.isArray(value)) { @@ -414,7 +415,7 @@ export const transformData = ( } } return acc; - }, {} as any); + }, Promise.resolve({}) as any); }; /** From 81b2e54b40b212dd9fdd7211e1d57bd6ced3c176 Mon Sep 17 00:00:00 2001 From: Regourd Colin Date: Mon, 7 Oct 2024 12:04:11 +0200 Subject: [PATCH 3/4] Nullable realtionship (#473) --- .changeset/wise-snakes-boil.md | 5 +++++ packages/next-admin/src/components/inputs/SelectWidget.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/wise-snakes-boil.md diff --git a/.changeset/wise-snakes-boil.md b/.changeset/wise-snakes-boil.md new file mode 100644 index 00000000..88b42a31 --- /dev/null +++ b/.changeset/wise-snakes-boil.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Fix relation one-to-many - nullable relation diff --git a/packages/next-admin/src/components/inputs/SelectWidget.tsx b/packages/next-admin/src/components/inputs/SelectWidget.tsx index 5c57b740..e86747a7 100644 --- a/packages/next-admin/src/components/inputs/SelectWidget.tsx +++ b/packages/next-admin/src/components/inputs/SelectWidget.tsx @@ -36,7 +36,7 @@ const SelectWidget = ({ const { basePath } = useConfig(); - const handleChange = (option: Enumeration) => { + const handleChange = (option: Enumeration | null) => { setFieldDirty(props.name); onChange(option); onClose(); @@ -95,7 +95,7 @@ const SelectWidget = ({ className="flex items-center" onClick={(e) => { e.preventDefault(); - onChange({}); + handleChange(null); }} > From 0d2b092f83cc4820a3c6651727b64bb40519a34e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 12:37:33 +0200 Subject: [PATCH 4/4] Version Packages (#474) Co-authored-by: github-actions[bot] --- .changeset/dry-tips-thank.md | 5 ----- .changeset/wise-snakes-boil.md | 5 ----- .changeset/witty-snakes-shout.md | 5 ----- apps/docs/package.json | 2 +- apps/docs/pages/changelog/index.md | 8 ++++++++ apps/example/package.json | 2 +- packages/next-admin/CHANGELOG.md | 8 ++++++++ packages/next-admin/package.json | 2 +- yarn.lock | 6 +++--- 9 files changed, 22 insertions(+), 21 deletions(-) delete mode 100644 .changeset/dry-tips-thank.md delete mode 100644 .changeset/wise-snakes-boil.md delete mode 100644 .changeset/witty-snakes-shout.md diff --git a/.changeset/dry-tips-thank.md b/.changeset/dry-tips-thank.md deleted file mode 100644 index a5d34e75..00000000 --- a/.changeset/dry-tips-thank.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@premieroctet/next-admin": patch ---- - -feat: add depth selection for actions (#443) diff --git a/.changeset/wise-snakes-boil.md b/.changeset/wise-snakes-boil.md deleted file mode 100644 index 88b42a31..00000000 --- a/.changeset/wise-snakes-boil.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@premieroctet/next-admin": patch ---- - -Fix relation one-to-many - nullable relation diff --git a/.changeset/witty-snakes-shout.md b/.changeset/witty-snakes-shout.md deleted file mode 100644 index 218d6418..00000000 --- a/.changeset/witty-snakes-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@premieroctet/next-admin": patch ---- - -Fix image (get async) diff --git a/apps/docs/package.json b/apps/docs/package.json index afe390d2..ab170e57 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@heroicons/react": "^2.1.1", - "@premieroctet/next-admin": "6.1.5", + "@premieroctet/next-admin": "6.1.6", "clsx": "^2.1.0", "framer-motion": "^11.0.8", "mini-svg-data-uri": "^1.4.4", diff --git a/apps/docs/pages/changelog/index.md b/apps/docs/pages/changelog/index.md index b1fdf5a4..c813b54f 100644 --- a/apps/docs/pages/changelog/index.md +++ b/apps/docs/pages/changelog/index.md @@ -1,5 +1,13 @@ # @premieroctet/next-admin +## 6.1.6 + +### Patch Changes + +- [56ea03b](https://github.com/premieroctet/next-admin/commit/56ea03b): feat: add depth selection for actions ([#443](https://github.com/premieroctet/next-admin/issues/443)) +- [81b2e54](https://github.com/premieroctet/next-admin/commit/81b2e54): Fix relation one-to-many - nullable relation +- [3225788](https://github.com/premieroctet/next-admin/commit/3225788): Fix image (get async) + ## 6.1.5 ### Patch Changes diff --git a/apps/example/package.json b/apps/example/package.json index 8b24c189..393dbd1b 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -20,7 +20,7 @@ "dependencies": { "@heroicons/react": "^2.0.18", "@picocss/pico": "^1.5.7", - "@premieroctet/next-admin": "6.1.5", + "@premieroctet/next-admin": "6.1.6", "@prisma/client": "5.14.0", "@tremor/react": "^3.2.2", "babel-plugin-superjson-next": "^0.4.5", diff --git a/packages/next-admin/CHANGELOG.md b/packages/next-admin/CHANGELOG.md index b1fdf5a4..c813b54f 100644 --- a/packages/next-admin/CHANGELOG.md +++ b/packages/next-admin/CHANGELOG.md @@ -1,5 +1,13 @@ # @premieroctet/next-admin +## 6.1.6 + +### Patch Changes + +- [56ea03b](https://github.com/premieroctet/next-admin/commit/56ea03b): feat: add depth selection for actions ([#443](https://github.com/premieroctet/next-admin/issues/443)) +- [81b2e54](https://github.com/premieroctet/next-admin/commit/81b2e54): Fix relation one-to-many - nullable relation +- [3225788](https://github.com/premieroctet/next-admin/commit/3225788): Fix image (get async) + ## 6.1.5 ### Patch Changes diff --git a/packages/next-admin/package.json b/packages/next-admin/package.json index 4d1b800a..b729ab51 100644 --- a/packages/next-admin/package.json +++ b/packages/next-admin/package.json @@ -1,6 +1,6 @@ { "name": "@premieroctet/next-admin", - "version": "6.1.5", + "version": "6.1.6", "description": "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 project.", "keywords": [ "next.js", diff --git a/yarn.lock b/yarn.lock index 5a6b693d..edf4ed7c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2729,7 +2729,7 @@ __metadata: languageName: unknown linkType: soft -"@premieroctet/next-admin@npm:6.1.5, @premieroctet/next-admin@workspace:packages/next-admin": +"@premieroctet/next-admin@npm:6.1.6, @premieroctet/next-admin@workspace:packages/next-admin": version: 0.0.0-use.local resolution: "@premieroctet/next-admin@workspace:packages/next-admin" dependencies: @@ -6949,7 +6949,7 @@ __metadata: dependencies: "@babel/core": "npm:^7.0.0" "@heroicons/react": "npm:^2.1.1" - "@premieroctet/next-admin": "npm:6.1.5" + "@premieroctet/next-admin": "npm:6.1.6" "@types/node": "npm:^17.0.12" "@types/react": "npm:^18.2.0" "@types/react-dom": "npm:^18.2.0" @@ -8012,7 +8012,7 @@ __metadata: "@heroicons/react": "npm:^2.0.18" "@picocss/pico": "npm:^1.5.7" "@playwright/test": "npm:^1.37.0" - "@premieroctet/next-admin": "npm:6.1.5" + "@premieroctet/next-admin": "npm:6.1.6" "@prisma/client": "npm:5.14.0" "@tremor/react": "npm:^3.2.2" "@types/node": "npm:^17.0.12"