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"