From e390d309fe6cc6b7bbb6bbe2f2d3cafb5fcda5a4 Mon Sep 17 00:00:00 2001
From: Hugo FOYART <11079152+foyarash@users.noreply.github.com>
Date: Wed, 5 Feb 2025 16:08:41 +0100
Subject: [PATCH] feat: add multifile upload (#532)
* feat: add multifile upload
* add page router support, start doc
* update docs
* fix page router form parse
* add images to post resource
* update generator deps
* fix mocks
* fix e2e
---
.changeset/gold-baboons-rhyme.md | 5 +
.../pages/docs/api/model-configuration.mdx | 30 +-
apps/example/options.tsx | 12 +-
apps/example/package.json | 3 +-
apps/example/pageRouterOptions.tsx | 12 +-
.../migration.sql | 2 +
apps/example/prisma/schema.prisma | 1 +
packages/generator-prisma/package.json | 3 +
packages/next-admin/src/appHandler.ts | 8 +-
packages/next-admin/src/components/Form.tsx | 89 +++--
.../src/components/inputs/ArrayField.tsx | 34 +-
.../src/components/inputs/FileWidget.tsx | 210 -----------
.../components/inputs/FileWidget/FileItem.tsx | 98 +++++
.../inputs/FileWidget/FileWidget.tsx | 197 ++++++++++
.../src/context/FormDataContext.tsx | 8 +-
packages/next-admin/src/handlers/resources.ts | 14 +-
packages/next-admin/src/pageHandler.ts | 2 +-
.../next-admin/src/tests/prismaUtils.test.ts | 4 +
packages/next-admin/src/types.ts | 38 +-
packages/next-admin/src/utils/server.ts | 337 +++++++++++++-----
packages/next-admin/src/utils/tools.ts | 20 +-
pnpm-lock.yaml | 88 +++--
22 files changed, 811 insertions(+), 404 deletions(-)
create mode 100644 .changeset/gold-baboons-rhyme.md
create mode 100644 apps/example/prisma/migrations/20250205141119_post_images_list/migration.sql
delete mode 100644 packages/next-admin/src/components/inputs/FileWidget.tsx
create mode 100644 packages/next-admin/src/components/inputs/FileWidget/FileItem.tsx
create mode 100644 packages/next-admin/src/components/inputs/FileWidget/FileWidget.tsx
diff --git a/.changeset/gold-baboons-rhyme.md b/.changeset/gold-baboons-rhyme.md
new file mode 100644
index 00000000..a1a646ab
--- /dev/null
+++ b/.changeset/gold-baboons-rhyme.md
@@ -0,0 +1,5 @@
+---
+"@premieroctet/next-admin": minor
+---
+
+feat: allow multifile upload (#519)
diff --git a/apps/docs/pages/docs/api/model-configuration.mdx b/apps/docs/pages/docs/api/model-configuration.mdx
index 25a62741..65983f1d 100644
--- a/apps/docs/pages/docs/api/model-configuration.mdx
+++ b/apps/docs/pages/docs/api/model-configuration.mdx
@@ -469,6 +469,15 @@ When you define a field, use the field's name as the key and the following objec
description:
"an optional string displayed in the input field as an error message in case of a failure during the upload handler",
},
+ {
+ name: "handler.deleteFile",
+ type: "Function",
+ description: (
+ <>
+ an async function that is used to remove a file from a remote provider. Takes the file URI as an argument.
+ >
+ )
+ },
{
name: "optionFormatter",
type: "Function",
@@ -671,6 +680,25 @@ The `actions` property is an array of objects that allows you to define a set of
]}
/>
+## `middlewares` property
+
+The `middlewares` property is an object of functions executed either before a record's update or deletion, where you can control if the deletion and update should happen or not. It can have the following properties:
+
+
({
);
}
- const body = await getFormValuesFromFormData(await req.formData());
+ const body = await getFormValuesFromFormData(
+ await req.formData(),
+ options?.model?.[resource]?.edit?.fields as EditFieldsOptions<
+ typeof resource
+ >
+ );
const id =
params[paramKey].length === 2
? formatId(resource, params[paramKey].at(-1)!)
diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx
index 5bd5531b..9fa80a0d 100644
--- a/packages/next-admin/src/components/Form.tsx
+++ b/packages/next-admin/src/components/Form.tsx
@@ -46,14 +46,14 @@ import {
Permission,
} from "../types";
import { getSchemas } from "../utils/jsonSchema";
-import { formatLabel, slugify } from "../utils/tools";
+import { formatLabel, isFileUploadFormat, slugify } from "../utils/tools";
import FormHeader from "./FormHeader";
import ArrayField from "./inputs/ArrayField";
import BaseInput from "./inputs/BaseInput";
import CheckboxWidget from "./inputs/CheckboxWidget";
import DateTimeWidget from "./inputs/DateTimeWidget";
import DateWidget from "./inputs/DateWidget";
-import FileWidget from "./inputs/FileWidget";
+import FileWidget from "./inputs/FileWidget/FileWidget";
import JsonField from "./inputs/JsonField";
import NullField from "./inputs/NullField";
import SelectWidget from "./inputs/SelectWidget";
@@ -229,20 +229,16 @@ const Form = ({
body: formData,
}
);
-
const result = await response.json();
-
if (result?.validation) {
setValidation(result.validation);
} else {
setValidation(undefined);
}
-
if (result?.data) {
setFormData(result.data);
cleanAll();
}
-
if (result?.deleted) {
return router.replace({
pathname: `${basePath}/${slugify(resource)}`,
@@ -254,7 +250,6 @@ const Form = ({
},
});
}
-
if (result?.created) {
const pathname = result?.redirect
? `${basePath}/${slugify(resource)}`
@@ -269,12 +264,10 @@ const Form = ({
},
});
}
-
if (result?.updated) {
const pathname = result?.redirect
? `${basePath}/${slugify(resource)}`
: location.pathname;
-
if (pathname === location.pathname) {
showMessage({
type: "success",
@@ -292,7 +285,6 @@ const Form = ({
});
}
}
-
if (result?.error) {
showMessage({
type: "error",
@@ -517,29 +509,60 @@ const Form = ({
},
};
- const CustomForm = forwardRef