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/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 8beaf4ec..3138f5c5 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"; @@ -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/CHANGELOG.md b/packages/next-admin/CHANGELOG.md index 47481304..621546c2 100644 --- a/packages/next-admin/CHANGELOG.md +++ b/packages/next-admin/CHANGELOG.md @@ -5,6 +5,13 @@ ### Major Changes - [1fa56bc](https://github.com/premieroctet/next-admin/commit/1fa56bc): feat: add custom generator ([#414](https://github.com/premieroctet/next-admin/issues/414)) +## 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 diff --git a/packages/next-admin/src/appHandler.ts b/packages/next-admin/src/appHandler.ts index 5012f98a..ca6bd4b9 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/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/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); }} > diff --git a/packages/next-admin/src/pageHandler.ts b/packages/next-admin/src/pageHandler.ts index ccca0eb7..3adc0888 100644 --- a/packages/next-admin/src/pageHandler.ts +++ b/packages/next-admin/src/pageHandler.ts @@ -86,7 +86,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 fbac6611..39bd21e8 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 f20b2775..4be96fa9 100644 --- a/packages/next-admin/src/utils/prisma.ts +++ b/packages/next-admin/src/utils/prisma.ts @@ -1,9 +1,6 @@ -import { $Enums, Prisma, PrismaClient } from "@prisma/client"; +import type { NextAdminJsonSchemaData } from "@premieroctet/next-admin-json-schema"; +import { Prisma, PrismaClient } from "@prisma/client"; import { cloneDeep } from "lodash"; -import type { - NextAdminJSONSchema, - NextAdminJsonSchemaData, -} from "@premieroctet/next-admin-json-schema"; import { ITEMS_PER_PAGE } from "../config"; import { EditOptions, @@ -22,17 +19,17 @@ import { Select, } from "../types"; import { validateQuery } from "./advancedSearch"; +import { getDefinitionFromRef } from "./jsonSchema"; import { enumValueForEnumType, findRelationInData, getModelIdProperty, getToStringForRelations, - modelHasIdField, globalSchema, + modelHasIdField, transformData, } from "./server"; import { capitalize, isScalar, uncapitalize } from "./tools"; -import { getDefinitionFromRef } from "./jsonSchema"; type CreateNestedWherePredicateParams = { field: NextAdminJsonSchemaData & { name: string }; @@ -702,6 +699,39 @@ export const getDataItem = async ({ return data; }; +type DeepIncludeRecord = Record; + +const includeDataByDepth = ( + modelProperties: SchemaDefinitions[ModelName]["properties"], + currentDepth: number, + maxDepth: number +) => { + const include = Object.entries(modelProperties)?.reduce( + (acc, [name, field]) => { + if (field.__nextadmin?.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) { + const nextModel = + globalSchema.definitions[field.__nextadmin.type as M].properties; + acc[name] = { + include: includeDataByDepth(nextModel, currentDepth + 1, maxDepth), + }; + } else { + acc[name] = true; + } + } + return acc; + }, + {} as DeepIncludeRecord + ); + + return include; +}; + /** * Get raw data from Prisma (2-deep nested relations) * @param prisma @@ -713,25 +743,19 @@ export const getRawData = async ({ prisma, resource, resourceIds, + maxDepth = 2, }: { prisma: PrismaClient; resource: M; resourceIds: Array; + maxDepth?: number; }): Promise[]> => { const model = globalSchema.definitions[ resource ] as SchemaDefinitions[ModelName]; const modelProperties = model.properties; - const include = Object.entries(modelProperties).reduce( - (acc, [name, field]) => { - if (field.__nextadmin?.kind === "object") { - acc[name] = true; - } - return acc; - }, - {} as Record - ); + const include = includeDataByDepth(modelProperties!, 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 215da428..96b8edb0 100644 --- a/packages/next-admin/src/utils/server.ts +++ b/packages/next-admin/src/utils/server.ts @@ -65,10 +65,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( @@ -348,7 +352,8 @@ export const transformData = ( const schemaProperties = model.properties; - return Object.keys(data).reduce((acc, key) => { + return Object.keys(data).reduce(async (accP, key) => { + const acc = await accP; const field = schemaProperties[key as keyof typeof schemaProperties]; const fieldKind = field?.__nextadmin?.kind; const get = editOptions?.fields?.[key as Field]?.handler?.get; @@ -357,7 +362,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)) { @@ -450,7 +455,7 @@ export const transformData = ( } } return acc; - }, {} as any); + }, Promise.resolve({}) as any); }; /** diff --git a/yarn.lock b/yarn.lock index f039b145..91613662 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2759,7 +2759,7 @@ __metadata: languageName: unknown linkType: soft -"@premieroctet/next-admin@npm:6.1.5, @premieroctet/next-admin@workspace:packages/next-admin": +"@premieroctet/next-admin@npm:7.0.0-rc.0, @premieroctet/next-admin@workspace:packages/next-admin": version: 0.0.0-use.local resolution: "@premieroctet/next-admin@workspace:packages/next-admin" dependencies: @@ -7075,7 +7075,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:7.0.0-rc.0" "@types/node": "npm:^17.0.12" "@types/react": "npm:^18.2.0" "@types/react-dom": "npm:^18.2.0" @@ -8138,7 +8138,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:7.0.0-rc.0" "@premieroctet/next-admin-generator-prisma": "workspace:*" "@prisma/client": "npm:5.14.0" "@tremor/react": "npm:^3.2.2"