(
@@ -35,56 +33,34 @@ const useSearchPaginatedResource = ({
try {
setIsPending(true);
- if (isAppDir) {
- const response = await searchPaginatedResourceAction?.({
+ const response = await fetch(`${apiBasePath}/options`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
originModel: resource!,
property: fieldName,
model,
query,
page: searchPage.current,
perPage,
- });
+ }),
+ });
- if (response && !response.error) {
- totalSearchedItems.current = response.total;
+ if (response.ok) {
+ const responseJson = await response.json();
+
+ if (!responseJson.error) {
+ totalSearchedItems.current = responseJson.total;
setAllOptions((old) => {
if (resetOptions) {
- return response.data;
+ return responseJson.data;
}
- return [...old, ...response.data] as Enumeration[];
+ return [...old, ...responseJson.data];
});
}
- } else {
- const response = await fetch(`${basePath}/api/options`, {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- },
- body: JSON.stringify({
- originModel: resource!,
- property: fieldName,
- model,
- query,
- page: searchPage.current,
- perPage,
- }),
- });
-
- if (response.ok) {
- const responseJson = await response.json();
-
- if (!responseJson.error) {
- totalSearchedItems.current = responseJson.total;
- setAllOptions((old) => {
- if (resetOptions) {
- return responseJson.data;
- }
-
- return [...old, ...responseJson.data];
- });
- }
- }
}
} finally {
setIsPending(false);
diff --git a/packages/next-admin/src/index.tsx b/packages/next-admin/src/index.ts
similarity index 100%
rename from packages/next-admin/src/index.tsx
rename to packages/next-admin/src/index.ts
index 1c37010e..eb9c0e30 100644
--- a/packages/next-admin/src/index.tsx
+++ b/packages/next-admin/src/index.ts
@@ -1,3 +1,3 @@
-export * from "./types";
-export * from "./components/NextAdmin";
export * from "./components/MainLayout";
+export * from "./components/NextAdmin";
+export * from "./types";
diff --git a/packages/next-admin/src/mainLayout.tsx b/packages/next-admin/src/mainLayout.tsx
deleted file mode 100644
index 44333506..00000000
--- a/packages/next-admin/src/mainLayout.tsx
+++ /dev/null
@@ -1 +0,0 @@
-export { getMainLayoutProps } from "./utils/props";
diff --git a/packages/next-admin/src/pageHandler.ts b/packages/next-admin/src/pageHandler.ts
new file mode 100644
index 00000000..f43a2709
--- /dev/null
+++ b/packages/next-admin/src/pageHandler.ts
@@ -0,0 +1,223 @@
+import { PrismaClient } from "@prisma/client";
+import { NextApiRequest, NextApiResponse } from "next";
+import { NextHandler, createRouter } from "next-connect";
+import { handleOptionsSearch } from "./handlers/options";
+import { deleteResource, submitResource } from "./handlers/resources";
+import { NextAdminOptions, Permission } from "./types";
+import { hasPermission } from "./utils/permissions";
+import {
+ formatId,
+ getFormDataValues,
+ getJsonBody,
+ getResourceFromParams,
+ getResources,
+} from "./utils/server";
+
+type CreateAppHandlerParams = {
+ /**
+ * `apiBasePath` is a string that represents the base path of the admin API route. (e.g. `/api`) - optional.
+ */
+ apiBasePath: string;
+ /**
+ * Next-admin options
+ */
+ options?: NextAdminOptions;
+ /**
+ * Prisma client instance
+ */
+ prisma: PrismaClient;
+ /**
+ * A function that acts as a middleware. Useful to add authentication logic for example.
+ */
+ onRequest?: (
+ req: NextApiRequest,
+ res: NextApiResponse,
+ next: NextHandler
+ ) => Promise;
+ // /**
+ // * A string indicating the name of the dynamic segment.
+ // *
+ // * Example:
+ // * - If the dynamic segment is `[[...nextadmin]]`, then the `paramKey` should be `nextadmin`.
+ // * - If the dynamic segment is `[[...admin]]`, then the `paramKey` should be `admin`.
+ // *
+ // * @default "nextadmin"
+ // */
+ paramKey?: P;
+ /**
+ * Generated JSON schema from Prisma
+ */
+ schema: any;
+};
+
+export const createHandler = ({
+ apiBasePath,
+ options,
+ prisma,
+ paramKey = "nextadmin" as P,
+ onRequest,
+ schema,
+}: CreateAppHandlerParams
) => {
+ const router = createRouter();
+ const resources = getResources(options);
+
+ if (onRequest) {
+ router.use(onRequest);
+ }
+
+ router
+ .post(`${apiBasePath}/:model/actions/:id`, async (req, res) => {
+ const id = req.query[paramKey]!.at(-1)!;
+
+ // Make sure we don't have a false positive with a model that could be named actions
+ const resource = getResourceFromParams(
+ [req.query[paramKey]![0]],
+ resources
+ );
+
+ if (!resource) {
+ return res.status(404).json({ error: "Resource not found" });
+ }
+
+ const modelAction = options?.model?.[resource]?.actions?.find(
+ (action) => action.id === id
+ );
+
+ if (!modelAction) {
+ return res.status(404).json({ error: "Action not found" });
+ }
+
+ let body;
+
+ try {
+ body = await getJsonBody(req);
+ } catch {
+ return res.status(400).json({ error: "Invalid JSON body" });
+ }
+
+ try {
+ await modelAction.action(body);
+
+ return res.json({ ok: true });
+ } catch (e) {
+ return res.status(500).json({ error: (e as Error).message });
+ }
+ })
+ .post(`${apiBasePath}/options`, async (req, res) => {
+ let body;
+
+ try {
+ body = await getJsonBody(req);
+ } catch {
+ return res.status(400).json({ error: "Invalid JSON body" });
+ }
+
+ const data = await handleOptionsSearch(body, prisma, options);
+
+ return res.json(data);
+ })
+ .post(`${apiBasePath}/:model/:id?`, async (req, res) => {
+ const resource = getResourceFromParams(
+ [req.query[paramKey]![0]],
+ resources
+ );
+
+ if (!resource) {
+ return res.status(404).json({ error: "Resource not found" });
+ }
+
+ const body = await getFormDataValues(req);
+ const id =
+ req.query[paramKey]!.length === 2
+ ? formatId(resource, req.query[paramKey]!.at(-1)!)
+ : undefined;
+
+ try {
+ const response = await submitResource({
+ prisma,
+ resource,
+ body,
+ id,
+ options,
+ schema,
+ });
+
+ if (response.error) {
+ return res.status(400).json({
+ error: response.error,
+ validation: response.validation,
+ });
+ }
+
+ return res.status(id ? 200 : 201).json(response);
+ } catch (e) {
+ return res.status(500).json({ error: (e as Error).message });
+ }
+ })
+ .delete(`${apiBasePath}/:model/:id`, async (req, res) => {
+ const resource = getResourceFromParams(
+ [req.query[paramKey]![0]],
+ resources
+ );
+
+ if (!resource) {
+ return res.status(404).json({ error: "Resource not found" });
+ }
+
+ if (!hasPermission(options?.model?.[resource], Permission.DELETE)) {
+ return res.status(403).json({
+ error: "You don't have permission to delete this resource",
+ });
+ }
+
+ try {
+ await deleteResource({
+ body: [req.query[paramKey]![1]],
+ prisma,
+ resource,
+ });
+
+ return res.json({ ok: true });
+ } catch (e) {
+ return res.status(500).json({ error: (e as Error).message });
+ }
+ })
+ .delete(`${apiBasePath}/:model`, async (req, res) => {
+ const resource = getResourceFromParams(
+ [req.query[paramKey]![0]],
+ resources
+ );
+
+ if (!resource) {
+ return res.status(404).json({ error: "Resource not found" });
+ }
+
+ if (!hasPermission(options?.model?.[resource], Permission.DELETE)) {
+ return res.status(403).json({
+ error: "You don't have permission to delete this resource",
+ });
+ }
+
+ let body;
+
+ try {
+ body = await getJsonBody(req);
+ } catch {
+ return res.status(400).json({ error: "Invalid JSON body" });
+ }
+
+ try {
+ await deleteResource({ body, prisma, resource });
+
+ return res.json({ ok: true });
+ } catch (e) {
+ return res.status(500).json({ error: (e as Error).message });
+ }
+ });
+
+ const executeRouteHandler = (req: NextApiRequest, res: NextApiResponse) => {
+ return router.run(req, res);
+ };
+
+ return { run: executeRouteHandler, router };
+};
diff --git a/packages/next-admin/src/pageRouter.ts b/packages/next-admin/src/pageRouter.ts
new file mode 100644
index 00000000..54638624
--- /dev/null
+++ b/packages/next-admin/src/pageRouter.ts
@@ -0,0 +1,38 @@
+import { IncomingMessage } from "node:http";
+import { GetMainLayoutPropsParams, GetNextAdminPropsParams } from "./types";
+import {
+ getMainLayoutProps as _getMainLayoutProps,
+ getPropsFromParams,
+} from "./utils/props";
+import { formatSearchFields, getParamsFromUrl } from "./utils/server";
+
+// Router
+export const getNextAdminProps = async ({
+ prisma,
+ schema,
+ basePath,
+ apiBasePath,
+ options,
+ req,
+}: Omit & {
+ req: IncomingMessage;
+}) => {
+ const params = getParamsFromUrl(req.url!, basePath);
+ const requestOptions = formatSearchFields(req.url!);
+
+ const props = await getPropsFromParams({
+ options,
+ prisma,
+ schema,
+ basePath,
+ apiBasePath,
+ searchParams: requestOptions,
+ params,
+ isAppDir: false,
+ });
+
+ return { props };
+};
+
+
+export const getMainLayoutProps = (args: Omit) => _getMainLayoutProps({ ...args, isAppDir: false });
\ No newline at end of file
diff --git a/packages/next-admin/src/router.tsx b/packages/next-admin/src/router.tsx
deleted file mode 100644
index 3dbecfc9..00000000
--- a/packages/next-admin/src/router.tsx
+++ /dev/null
@@ -1,379 +0,0 @@
-import { PrismaClient } from "@prisma/client";
-import {
- PrismaClientKnownRequestError,
- PrismaClientValidationError,
-} from "@prisma/client/runtime/library";
-import { createRouter } from "next-connect";
-
-import { EditFieldsOptions, NextAdminOptions, Permission } from "./types";
-import { optionsFromResource } from "./utils/prisma";
-import { getPropsFromParams } from "./utils/props";
-import {
- formatSearchFields,
- formattedFormData,
- getBody,
- getFormDataValues,
- getModelIdProperty,
- getParamsFromUrl,
- getPrismaModelForResource,
- getResourceFromParams,
- getResourceIdFromParam,
- getResources,
- parseFormData,
-} from "./utils/server";
-import { slugify, uncapitalize } from "./utils/tools";
-import { validate } from "./utils/validator";
-
-// Router
-export const nextAdminRouter = async (
- prisma: PrismaClient,
- schema: any,
- options: NextAdminOptions
-) => {
- const resources = getResources(options);
- const defaultProps = { resources, basePath: options.basePath };
-
- return (
- createRouter()
- // Error handling middleware
- .use(async (req, res, next) => {
- try {
- return await next();
- } catch (e: any) {
- if (process.env.NODE_ENV === "development") {
- throw e;
- }
-
- return {
- props: { ...defaultProps, error: e.message },
- };
- }
- })
- .get(async (req, res) => {
- const params = getParamsFromUrl(req.url!, options.basePath);
-
- const requestOptions = formatSearchFields(req.url!);
-
- const props = await getPropsFromParams({
- options,
- prisma,
- schema,
- searchParams: requestOptions,
- params,
- isAppDir: false,
- });
-
- return { props };
- })
- .post(`${options.basePath}/api/options`, async (req, res) => {
- const body = await getBody(req);
- const { originModel, property, model, query, page, perPage } =
- JSON.parse(body) as any;
-
- const data = await optionsFromResource({
- prisma,
- originResource: originModel,
- property: property,
- resource: model,
- options,
- context: {},
- searchParams: new URLSearchParams({
- search: query,
- page: page.toString(),
- itemsPerPage: perPage.toString(),
- }),
- appDir: false,
- });
-
- res.setHeader("Content-Type", "application/json");
- res.write(JSON.stringify(data));
- res.end();
- })
- .post(async (req, res) => {
- const params = getParamsFromUrl(req.url!, options.basePath);
- const message = req.url?.split("?message=")[1];
-
- const resource = getResourceFromParams(params, resources);
- const requestOptions = formatSearchFields(req.url!);
-
- if (!resource) {
- return { notFound: true };
- }
-
- const resourceId = getResourceIdFromParam(params[1], resource);
-
- const getProps = () =>
- getPropsFromParams({
- options,
- prisma,
- schema,
- searchParams: requestOptions,
- params,
- isAppDir: false,
- });
-
- const {
- __admin_action: action,
- __admin_redirect: redirect,
- id,
- ...formData
- } = await getFormDataValues(req);
-
- const dmmfSchema = getPrismaModelForResource(resource);
-
- const parsedFormData = parseFormData(formData, dmmfSchema?.fields!);
-
- const modelIdProperty = getModelIdProperty(resource);
-
- try {
- // Delete redirect, display the list (this is needed because next keeps the HTTP method on redirects)
- if (
- !resourceId &&
- params[1] !== "new" &&
- (action === "delete" || redirect)
- ) {
- if (message) {
- return {
- props: {
- ...(await getProps()),
- resource,
- message: JSON.parse(decodeURIComponent(message)),
- },
- };
- }
-
- return {
- props: {
- ...(await getProps()),
- resource,
- },
- };
- }
-
- // Delete
- if (resourceId !== undefined && action === "delete") {
- if (
- options?.model?.[resource]?.permissions &&
- !options?.model?.[resource]?.permissions?.includes(
- Permission.DELETE
- )
- ) {
- res.statusCode = 403;
- return {
- props: {
- ...(await getProps()),
- error: "Unable to delete items of this model",
- },
- };
- }
-
- // @ts-expect-error
- await prisma[resource].delete({
- where: {
- [modelIdProperty]: resourceId,
- },
- });
- const message = {
- type: "success",
- content: "Deleted successfully",
- };
- return {
- redirect: {
- destination: `${options.basePath}/${slugify(
- resource
- )}?message=${JSON.stringify(message)}`,
- permanent: false,
- },
- };
- }
-
- const fields = options.model?.[resource]?.edit
- ?.fields as EditFieldsOptions;
-
- // Validate
- validate(parsedFormData, fields);
-
- const { formattedData, complementaryFormattedData, errors } =
- await formattedFormData(
- formData,
- dmmfSchema?.fields!,
- schema,
- resource,
- resourceId,
- fields
- );
-
- if (errors.length) {
- return {
- props: {
- ...(await getProps()),
- error:
- options.model?.[resource]?.edit?.submissionErrorMessage ??
- "Submission error",
- validation: errors.map((error) => ({
- property: error.field,
- message: error.message,
- })),
- },
- };
- }
-
- if (resourceId !== undefined) {
- if (
- options?.model?.[resource]?.permissions &&
- !options?.model?.[resource]?.permissions?.includes(
- Permission.EDIT
- )
- ) {
- res.statusCode = 403;
- return {
- props: {
- ...(await getProps()),
- error: "Unable to update items of this model",
- },
- };
- }
-
- // @ts-expect-error
- await prisma[resource].update({
- where: {
- [modelIdProperty]: resourceId,
- },
- data: formattedData,
- });
-
- const message = {
- type: "success",
- content: "Updated successfully",
- };
-
- if (redirect) {
- return {
- redirect: {
- destination: `${options.basePath}/${slugify(
- resource
- )}?message=${JSON.stringify(message)}`,
- permanent: false,
- },
- };
- } else {
- return {
- props: {
- ...(await getProps()),
- message,
- },
- };
- }
- }
-
- // Create
- if (
- options?.model?.[resource]?.permissions &&
- options?.model?.[resource]?.permissions?.includes(Permission.CREATE)
- ) {
- res.statusCode = 403;
- return {
- props: {
- ...(await getProps()),
- error: "Unable to create items of this model",
- },
- };
- }
-
- // @ts-expect-error
- const createdData = await prisma[resource].create({
- data: formattedData,
- });
-
- // @ts-expect-error
- await prisma[resource].update({
- where: {
- [modelIdProperty]: createdData[modelIdProperty],
- },
- data: complementaryFormattedData,
- });
-
- const pathname = redirect
- ? `${options.basePath}/${slugify(resource)}`
- : `${options.basePath}/${slugify(resource)}/${
- createdData[modelIdProperty]
- }`;
- return {
- redirect: {
- destination: `${pathname}?message=${JSON.stringify({
- type: "success",
- content: "Created successfully",
- })}`,
- permanent: false,
- },
- };
- } catch (error: any) {
- if (
- error.constructor.name === PrismaClientValidationError.name ||
- error.constructor.name === PrismaClientKnownRequestError.name ||
- error.name === "ValidationError"
- ) {
- let data = parsedFormData;
-
- // TODO This could be improved by merging form values but it's breaking stuff
- if (error.name === "ValidationError") {
- error.errors.map((error: any) => {
- // @ts-expect-error
- data[error.property] = formData[error.property];
- });
- }
-
- return {
- props: {
- ...(await getProps()),
- error: error.message,
- validation: error.errors,
- },
- };
- }
-
- throw error;
- }
- })
- .delete(async (req, res) => {
- const params = getParamsFromUrl(req.url!, options.basePath);
- const resource = getResourceFromParams(params, resources);
-
- if (!resource) {
- return { notFound: true };
- }
-
- const body = await getBody(req);
- const bodyJson = JSON.parse(body) as string[] | number[];
-
- const modelIdProperty = getModelIdProperty(resource);
-
- // @ts-expect-error
- await prisma[uncapitalize(resource)].deleteMany({
- where: {
- [modelIdProperty]: {
- in: bodyJson,
- },
- },
- });
-
- return {
- props: {
- ...(await getPropsFromParams({
- searchParams: formatSearchFields(req.url!),
- options,
- prisma,
- schema,
- params,
- isAppDir: false,
- })),
- resource,
- message: {
- type: "success",
- content: "Deleted successfully",
- },
- },
- };
- })
- );
-};
diff --git a/packages/next-admin/src/tests/prismaUtils.test.ts b/packages/next-admin/src/tests/prismaUtils.test.ts
index c0428ff6..6a00d904 100644
--- a/packages/next-admin/src/tests/prismaUtils.test.ts
+++ b/packages/next-admin/src/tests/prismaUtils.test.ts
@@ -60,7 +60,7 @@ describe("optionsFromResource", () => {
author: 1,
authorId: 1,
rate: new Decimal(5),
- order: 0
+ order: 0,
},
{
id: 2,
@@ -70,7 +70,7 @@ describe("optionsFromResource", () => {
author: 1,
authorId: 1,
rate: new Decimal(5),
- order: 1
+ order: 1,
},
];
diff --git a/packages/next-admin/src/tests/singleton.tsx b/packages/next-admin/src/tests/singleton.tsx
index 80a1e4b5..ea9661bb 100644
--- a/packages/next-admin/src/tests/singleton.tsx
+++ b/packages/next-admin/src/tests/singleton.tsx
@@ -670,7 +670,6 @@ export const schema: Schema = {
};
export const options: NextAdminOptions = {
- basePath: "/admin",
model: {
User: {
toString: (user) => `${user.name} (${user.email})`,
diff --git a/packages/next-admin/src/types.ts b/packages/next-admin/src/types.ts
index a9d08715..172ec4d3 100644
--- a/packages/next-admin/src/types.ts
+++ b/packages/next-admin/src/types.ts
@@ -1,8 +1,8 @@
import * as OutlineIcons from "@heroicons/react/24/outline";
import { Prisma, PrismaClient } from "@prisma/client";
import type { JSONSchema7 } from "json-schema";
+import { NextRequest, NextResponse } from "next/server";
import type { ChangeEvent, ReactNode } from "react";
-import type { SearchPaginatedResourceParams } from "./actions";
import type { PropertyValidationError } from "./exceptions/ValidationError";
declare type JSONSchema7Definition = JSONSchema7 & {
@@ -185,7 +185,7 @@ export type EditFieldsOptions = {
*/
handler?: Handler[P]>;
/**
- * a React Element that should receive `CustomInputProps`](#custominputprops)`. For App Router, this element must be a client component.
+ * a React Element that should receive [`CustomInputProps`](#custominputprops). For App Router, this element must be a client component. Don't set any props, they will be passed automatically to the component.
*/
input?: React.ReactElement;
/**
@@ -239,22 +239,28 @@ export type Handler<
* @param file
* @returns
*/
- upload?: (buffer: Buffer, infos: {
- name: string;
- type: string | null;
- }) => Promise;
+ upload?: (
+ buffer: Buffer,
+ infos: {
+ name: string;
+ type: string | null;
+ }
+ ) => Promise;
/**
* an optional string displayed in the input field as an error message in case of a failure during the upload handler.
*/
uploadErrorMessage?: string;
};
-export type UploadParameters = Parameters<(buffer: Buffer, infos: {
- name: string;
- type: string | null;
-}) => Promise>
-
-
+export type UploadParameters = Parameters<
+ (
+ buffer: Buffer,
+ infos: {
+ name: string;
+ type: string | null;
+ }
+ ) => Promise
+>;
export type RichTextFormat = "html" | "json";
@@ -269,6 +275,7 @@ export type FormatOptions = T extends string
| "date"
| "date-time"
| "time"
+ | "time-second"
| "alt-datetime"
| "alt-date"
| "file"
@@ -367,7 +374,8 @@ export type ActionStyle = "default" | "destructive";
export type ModelAction = {
title: string;
- action: (resource: ModelName, ids: string[] | number[]) => Promise;
+ id: string;
+ action: (ids: string[] | number[]) => Promise;
style?: ActionStyle;
successMessage?: string;
errorMessage?: string;
@@ -447,10 +455,6 @@ export type ExternalLink = {
};
export type NextAdminOptions = {
- /**
- * `basePath` is a string that represents the base path of your admin. (e.g. `/admin`) - optional.
- */
- basePath: string;
/**
* Global admin title
*
@@ -596,11 +600,12 @@ export type UserData = {
export type AdminUser = {
data: UserData;
- logoutUrl: string;
+ logout?: [RequestInfo, RequestInit?] | (() => void | Promise) | string;
};
export type AdminComponentProps = {
basePath: string;
+ apiBasePath: string;
schema?: Schema;
data?: ListData;
resource?: ModelName;
@@ -619,7 +624,6 @@ export type AdminComponentProps = {
dmmfSchema?: readonly Prisma.DMMF.Field[];
isAppDir?: boolean;
locale?: string;
- action?: (formData: FormData) => Promise;
/**
* Mandatory for page router
*/
@@ -633,16 +637,8 @@ export type AdminComponentProps = {
*/
pageComponent?: React.ComponentType;
customPages?: Array<{ title: string; path: string; icon?: ModelIcon }>;
- actions?: ModelAction[];
- deleteAction?: (model: ModelName, ids: string[] | number[]) => Promise;
+ actions?: Omit[];
translations?: Translations;
- searchPaginatedResourceAction?: (
- params: SearchPaginatedResourceParams
- ) => Promise<{
- data: Enumeration[];
- total: number;
- error: string | null;
- }>;
/**
* Global admin title
*
@@ -661,6 +657,7 @@ export type MainLayoutProps = Pick<
| "resourcesTitles"
| "customPages"
| "basePath"
+ | "apiBasePath"
| "isAppDir"
| "translations"
| "locale"
@@ -670,6 +667,8 @@ export type MainLayoutProps = Pick<
| "user"
| "externalLinks"
| "options"
+ | "resourcesIdProperty"
+ | "dmmfSchema"
>;
export type CustomUIProps = {
@@ -732,6 +731,117 @@ export type Translations = {
[key: string]: string;
};
-export const colorSchemes = ["light", "dark", "system"] as const;
+export const colorSchemes = ["light", "dark", "system"];
export type ColorScheme = (typeof colorSchemes)[number];
export type BasicColorScheme = Exclude;
+
+export type PageProps = Readonly<{
+ params: { [key: string]: string[] | string };
+ searchParams: { [key: string]: string | string[] | undefined } | undefined;
+}>;
+
+export type GetNextAdminPropsParams = {
+ /**
+ * `params` is an array of strings that represents the dynamic segments of your route. (e.g. `[[...params]]`)
+ */
+ params?: string | string[];
+ /**
+ * `searchParams` is an object that represents the query parameters of your route. (e.g. `?key=value`)
+ */
+ searchParams: { [key: string]: string | string[] | undefined } | undefined;
+ /**
+ * `basePath` is a string that represents the base path of your admin. (e.g. `/admin`)
+ */
+ basePath: string;
+ /**
+ * `apiBasePath` is a string that represents the base path of the admin API route. (e.g. `/api/admin`)
+ */
+ apiBasePath: string;
+ /**
+ * `options` is an object that represents the options of your admin.
+ * @link https://next-admin.js.org/docs/api-docs#next-admin-options
+ */
+ options?: NextAdminOptions;
+ /**
+ * `schema` is an object that represents the JSON schema of your Prisma schema.
+ */
+ schema: any;
+ /**
+ * `prisma` is an instance of PrismaClient.
+ */
+ prisma: PrismaClient;
+ isAppDir?: boolean;
+ /**
+ * `locale` is a string that represents the locale of your admin. (e.g. `en`)
+ */
+ locale?: string;
+ /**
+ * `getMessages` is a function that returns a promise of an object that represents the translations of your admin.
+ * @param locale
+ * @returns
+ */
+ getMessages?: (locale: string) => Promise>;
+};
+
+export type GetMainLayoutPropsParams = Omit<
+ GetNextAdminPropsParams,
+ "schema" | "searchParams" | "prisma"
+>;
+
+export type RequestContext = {
+ params: Record
;
+};
+
+export type CreateAppHandlerParams
= {
+ /**
+ * `apiBasePath` is a string that represents the base path of the admin API route. (e.g. `/api`) - optional.
+ */
+ apiBasePath: string;
+ /**
+ * Next-admin options
+ */
+ options?: NextAdminOptions;
+ /**
+ * Prisma client instance
+ */
+ prisma: PrismaClient;
+ /**
+ * A function that acts as a middleware. Useful to add authentication logic for example.
+ */
+ onRequest?: (
+ req: NextRequest,
+ ctx: RequestContext
+ ) =>
+ | ReturnType
+ | ReturnType
+ | Promise;
+ /**
+ * A string indicating the name of the dynamic segment.
+ *
+ * Example:
+ * - If the dynamic segment is `[[...nextadmin]]`, then the `paramKey` should be `nextadmin`.
+ * - If the dynamic segment is `[[...admin]]`, then the `paramKey` should be `admin`.
+ *
+ * @default "nextadmin"
+ */
+ paramKey?: P;
+ /**
+ * Generated JSON schema from Prisma
+ */
+ schema: any;
+};
+
+
+export type FormProps = {
+ data: any;
+ schema: any;
+ dmmfSchema: readonly Prisma.DMMF.Field[];
+ resource: ModelName;
+ slug?: string;
+ validation?: PropertyValidationError[];
+ title: string;
+ customInputs?: Record, React.ReactElement | undefined>;
+ actions?: AdminComponentProps["actions"];
+ icon?: ModelIcon;
+ resourcesIdProperty: Record;
+};
\ No newline at end of file
diff --git a/packages/next-admin/src/utils/actions.ts b/packages/next-admin/src/utils/actions.ts
deleted file mode 100644
index 81fd04a9..00000000
--- a/packages/next-admin/src/utils/actions.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { ActionParams } from "../types";
-
-/**
- * Following https://nextjs.org/docs/app/api-reference/functions/server-actions#binding-arguments
- * We need the params and schema options to be there when the action is called.
- * Other params (prisma, options) will be added by the app's action implementation.
- */
-export const createBoundServerAction = <
- Args extends any[],
- Params = ActionParams,
->(
- actionParams: Params,
- action: (params: Params, ...args: Args) => Promise
-) => {
- return action.bind(null, actionParams);
-};
diff --git a/packages/next-admin/src/utils/colorSchemeScript.ts b/packages/next-admin/src/utils/colorSchemeScript.ts
deleted file mode 100644
index c3e9ef72..00000000
--- a/packages/next-admin/src/utils/colorSchemeScript.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-// @ts-nocheck
-export const colorSchemeScript = (forceColorScheme, defaultColorScheme) => {
- document.documentElement.classList.remove("dark", "light");
- if (forceColorScheme) {
- return forceColorScheme;
- }
- const storedColorScheme = localStorage.getItem("next-admin-theme");
- const systemPreference = window.matchMedia("(prefers-color-scheme: dark)")
- .matches
- ? "dark"
- : "light";
-
- const colorSchemeValue =
- (storedColorScheme === "system" ? systemPreference : storedColorScheme) ??
- defaultColorScheme ??
- "system";
- document.documentElement.classList.add(colorSchemeValue);
-};
diff --git a/packages/next-admin/src/utils/file.test.ts b/packages/next-admin/src/utils/file.test.ts
new file mode 100644
index 00000000..77ed2257
--- /dev/null
+++ b/packages/next-admin/src/utils/file.test.ts
@@ -0,0 +1,136 @@
+import {
+ getFilenameAndExtensionFromUrl,
+ getFilenameFromUrl,
+ isBase64Url,
+ isImageType,
+} from "./file";
+
+describe("File utils", () => {
+ describe("isBase64Url", () => {
+ const base64Data =
+ "iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=";
+ it("should return true for full base64 url", () => {
+ expect(isBase64Url(`data:image/png;base64,${base64Data}`)).toBeTruthy();
+ });
+
+ it("should return false for website urls ", () => {
+ expect(isBase64Url("https://example.com/image.png")).toBeFalsy();
+ });
+
+ it("should return false for relative urls", () => {
+ expect(isBase64Url("/image.png")).toBeFalsy();
+ });
+
+ it("should return false for empty strings", () => {
+ expect(isBase64Url("")).toBeFalsy();
+ });
+
+ it("should return false for base64 strings without prefix", () => {
+ expect(isBase64Url(base64Data)).toBeFalsy();
+ });
+ });
+
+ describe("isImageType", () => {
+ it("should return true for image urls", () => {
+ expect(isImageType("https://example.com/image.png")).toBeTruthy();
+ });
+
+ it("should return true for image urls with query parameters", () => {
+ expect(
+ isImageType("https://example.com/image.jpg?width=100&height=100")
+ ).toBeTruthy();
+ });
+
+ it("should return true for base64 image urls", () => {
+ expect(
+ isImageType(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg=="
+ )
+ ).toBeTruthy();
+ });
+
+ it("should return false for non-image urls", () => {
+ expect(isImageType("https://example.com/document.pdf")).toBeFalsy();
+ });
+
+ it("should return false for urls without extensions", () => {
+ expect(isImageType("https://example.com/no-extension")).toBeFalsy();
+ });
+
+ it("should return false for empty strings", () => {
+ expect(isImageType("")).toBeFalsy();
+ });
+
+ it("should return true for image urls with uppercase extensions", () => {
+ expect(isImageType("https://example.com/image.PNG")).toBeTruthy();
+ });
+
+ it("should return true for image urls with anchor tags", () => {
+ expect(isImageType("https://example.com/image.png#anchor")).toBeTruthy();
+ });
+ });
+
+ describe("getFilenameAndExtensionFromUrl", () => {
+ it("should return filename and extension for a simple url", () => {
+ expect(
+ getFilenameAndExtensionFromUrl("https://example.com/image.jpg")
+ ).toEqual({
+ fileName: "image.jpg",
+ extension: "jpg",
+ });
+ });
+
+ it("should handle urls with query parameters", () => {
+ expect(
+ getFilenameAndExtensionFromUrl(
+ "https://example.com/document.pdf?version=1"
+ )
+ ).toEqual({
+ fileName: "document.pdf",
+ extension: "pdf",
+ });
+ });
+
+ it("should handle urls with hash", () => {
+ expect(
+ getFilenameAndExtensionFromUrl("https://example.com/page.html#section1")
+ ).toEqual({
+ fileName: "page.html",
+ extension: "html",
+ });
+ });
+
+ it("should return undefined for urls without filename", () => {
+ expect(getFilenameAndExtensionFromUrl("https://example.com/")).toEqual({
+ fileName: undefined,
+ extension: undefined,
+ });
+ });
+ });
+
+ describe("getFilenameFromUrl", () => {
+ it("should return filename for a simple url", () => {
+ expect(getFilenameFromUrl("https://example.com/image.jpg")).toBe(
+ "image.jpg"
+ );
+ });
+
+ it("should handle urls with query parameters", () => {
+ expect(
+ getFilenameFromUrl("https://example.com/document.pdf?version=1")
+ ).toBe("document.pdf");
+ });
+
+ it("should return undefined for base64 urls", () => {
+ expect(
+ getFilenameFromUrl(
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg=="
+ )
+ ).toBeUndefined();
+ });
+
+ it("should return undefined for urls without filename", () => {
+ expect(getFilenameFromUrl("https://example.com/")).toBeUndefined();
+ });
+ });
+});
diff --git a/packages/next-admin/src/utils/file.ts b/packages/next-admin/src/utils/file.ts
new file mode 100644
index 00000000..cf8ea035
--- /dev/null
+++ b/packages/next-admin/src/utils/file.ts
@@ -0,0 +1,74 @@
+const SUPPORTED_IMG_EXTENSIONS = [
+ "apng",
+ "avif",
+ "png",
+ "gif",
+ "jpg",
+ "jpeg",
+ "jfif",
+ "pjpeg",
+ "pjp",
+ "png",
+ "svg",
+ "webp",
+ "bmp",
+ "ico",
+ "cur",
+ "tif",
+ "tiff",
+];
+
+/**
+ * Check if the url is a base64 url
+ */
+export function isBase64Url(url: string) {
+ return url.startsWith("data:");
+}
+
+/*
+ * Get the filename and extension from a file url, does not work with base64 urls.
+ */
+export function getFilenameAndExtensionFromUrl(url: string) {
+ const cleanUrl = url.split("?")[0].split("#")[0];
+ const fullFilename = cleanUrl.split("/").pop();
+ if (!fullFilename) {
+ return {
+ fileName: undefined,
+ extension: undefined,
+ };
+ }
+ const [_, extension] = fullFilename.split(".");
+
+ return {
+ fileName: fullFilename,
+ extension,
+ };
+}
+
+/**
+ * Check if the url points to a file of type image
+ */
+export function isImageType(url: string) {
+ if (isBase64Url(url)) {
+ return url.split(":")[1].split(";")[0].includes("image");
+ } else {
+ const { extension } = getFilenameAndExtensionFromUrl(url);
+ if (!!extension) {
+ return SUPPORTED_IMG_EXTENSIONS.includes(extension.toLocaleLowerCase());
+ } else {
+ return false;
+ }
+ }
+}
+
+/**
+ * Get the filename from a file url
+*/
+export function getFilenameFromUrl(url: string) {
+ if (isBase64Url(url)) {
+ return undefined;
+ } else {
+ const { fileName } = getFilenameAndExtensionFromUrl(url);
+ return fileName;
+ }
+}
diff --git a/packages/next-admin/src/utils/jsonSchema.ts b/packages/next-admin/src/utils/jsonSchema.ts
index dd4750d4..0777440a 100644
--- a/packages/next-admin/src/utils/jsonSchema.ts
+++ b/packages/next-admin/src/utils/jsonSchema.ts
@@ -76,9 +76,9 @@ export function getSchemas(
if (
dmmfProperty &&
requiredFields?.includes(dmmfProperty.name) &&
- !schema.required?.includes(dmmfProperty.name)
+ !schema.require?.includes(dmmfProperty.name)
) {
- schema.required = [...schema.required, dmmfProperty.name];
+ schema.required = [...(schema.required ?? []), dmmfProperty.name];
}
if (
diff --git a/packages/next-admin/src/utils/options.ts b/packages/next-admin/src/utils/options.ts
index 107f2ada..99518786 100644
--- a/packages/next-admin/src/utils/options.ts
+++ b/packages/next-admin/src/utils/options.ts
@@ -7,9 +7,9 @@ import { Field, ModelName, NextAdminOptions } from "../types";
*/
export const getCustomInputs = (
model: ModelName,
- options: NextAdminOptions
+ options?: NextAdminOptions
) => {
- const editFields = options.model?.[model]?.edit?.fields;
+ const editFields = options?.model?.[model]?.edit?.fields;
return Object.keys(editFields ?? {}).reduce(
(acc, field) => {
diff --git a/packages/next-admin/src/utils/permissions.ts b/packages/next-admin/src/utils/permissions.ts
new file mode 100644
index 00000000..7e83cd78
--- /dev/null
+++ b/packages/next-admin/src/utils/permissions.ts
@@ -0,0 +1,11 @@
+import { ModelName, ModelOptions, Permission } from "../types";
+
+export const hasPermission = (
+ modelOptions: ModelOptions[ModelName],
+ permission: Permission
+) => {
+ return (
+ !modelOptions?.permissions ||
+ modelOptions?.permissions?.includes(permission)
+ );
+};
diff --git a/packages/next-admin/src/utils/prisma.ts b/packages/next-admin/src/utils/prisma.ts
index 0f5927b5..fdbd7e1c 100644
--- a/packages/next-admin/src/utils/prisma.ts
+++ b/packages/next-admin/src/utils/prisma.ts
@@ -21,10 +21,11 @@ import {
getPrismaModelForResource,
getToStringForRelations,
modelHasIdField,
+ transformData,
} from "./server";
import { capitalize, isScalar, uncapitalize } from "./tools";
-export const createWherePredicate = (
+const createWherePredicate = (
fieldsFiltered?: readonly Prisma.DMMF.Field[],
search?: string,
otherFilters?: Filter[]
@@ -76,7 +77,7 @@ export const createWherePredicate = (
return { AND: [...externalFilters, searchFilter] };
};
-export const getFieldsFiltered = (
+const getFieldsFiltered = (
resource: M,
options?: NextAdminOptions
): readonly Prisma.DMMF.Field[] => {
@@ -95,7 +96,7 @@ export const getFieldsFiltered = (
return fieldsFiltered as readonly Prisma.DMMF.Field[];
};
-export const preparePrismaListRequest = (
+const preparePrismaListRequest = (
resource: M,
searchParams: any,
options?: NextAdminOptions,
@@ -182,7 +183,7 @@ export const preparePrismaListRequest = (
type GetMappedDataListParams = {
prisma: PrismaClient;
resource: ModelName;
- options: NextAdminOptions;
+ options?: NextAdminOptions;
searchParams: URLSearchParams;
context: NextAdminContext;
appDir?: boolean;
@@ -199,7 +200,7 @@ export const optionsFromResource = async ({
...args
}: OptionsFromResourceParams) => {
const relationshipField =
- args.options.model?.[originResource]?.edit?.fields?.[
+ args.options?.model?.[originResource]?.edit?.fields?.[
property as Field
// @ts-expect-error
]?.relationshipSearchField;
@@ -260,11 +261,11 @@ export const optionsFromResource = async ({
type FetchDataListParams = {
prisma: PrismaClient;
resource: ModelName;
- options: NextAdminOptions;
+ options?: NextAdminOptions;
searchParams: URLSearchParams;
};
-export const fetchDataList = async (
+const fetchDataList = async (
{ prisma, resource, options, searchParams }: FetchDataListParams,
skipFilters: boolean = false
) => {
@@ -317,7 +318,7 @@ export const mapDataList = ({
const { resource, options } = args;
const dmmfSchema = getPrismaModelForResource(resource);
const data = findRelationInData(fetchData, dmmfSchema?.fields);
- const listFields = options.model?.[resource]?.list?.fields ?? {};
+ const listFields = options?.model?.[resource]?.list?.fields ?? {};
const originalData = cloneDeep(data);
data.forEach((item, index) => {
context.row = originalData[index];
@@ -437,27 +438,84 @@ export const selectPayloadForModel = (
return selectedFields;
};
-export const includeOrderByPayloadForModel = (
- resource: M,
- options: EditOptions
-) => {
- const model = getPrismaModelForResource(resource);
+export const getDataItem = async ({
+ prisma,
+ resource,
+ options,
- let orderedFields = model?.fields.reduce(
- (acc, field) => {
- // @ts-expect-error
- if (options.fields?.[field.name as Field]?.orderField) {
- acc[field.name] = {
- orderBy: {
- // @ts-expect-error
- [options.fields[field.name as Field].orderField]: "asc",
- },
+ resourceId,
+ locale,
+ isAppDir,
+}: {
+ options?: NextAdminOptions;
+ prisma: PrismaClient;
+ isAppDir?: boolean;
+ locale?: string;
+ resource: M;
+
+ resourceId: string | number;
+}) => {
+ const dmmfSchema = getPrismaModelForResource(resource);
+ const edit = options?.model?.[resource]?.edit as EditOptions;
+ const idProperty = getModelIdProperty(resource);
+ const select = selectPayloadForModel(resource, edit, "object");
+
+ Object.entries(select).forEach(([key, value]) => {
+ const fieldTypeDmmf = dmmfSchema?.fields.find((field) => field.name === key)
+ ?.type;
+
+ if (fieldTypeDmmf && dmmfSchema) {
+ const relatedResourceOptions =
+ options?.model?.[fieldTypeDmmf as ModelName]?.list;
+
+ if (
+ // @ts-expect-error
+ edit?.fields?.[key as Field]?.display === "table"
+ ) {
+ if (!relatedResourceOptions?.display) {
+ throw new Error(
+ `'table' display mode set for field '${key}', but no list display is setup for model ${fieldTypeDmmf}`
+ );
+ }
+
+ select[key] = {
+ select: selectPayloadForModel(
+ fieldTypeDmmf as ModelName,
+ relatedResourceOptions as ListOptions,
+ "object"
+ ),
};
}
- return acc;
- },
- {} as Record
- );
+ }
+ });
- return orderedFields;
+ // @ts-expect-error
+ let data = await prisma[resource].findUniqueOrThrow({
+ select,
+ where: { [idProperty]: resourceId },
+ });
+ Object.entries(data).forEach(([key, value]) => {
+ if (Array.isArray(value)) {
+ const fieldTypeDmmf = dmmfSchema?.fields.find(
+ (field) => field.name === key
+ )?.type;
+
+ if (fieldTypeDmmf && dmmfSchema) {
+ if (
+ // @ts-expect-error
+ edit?.fields?.[key as Field]?.display === "table"
+ ) {
+ data[key] = mapDataList({
+ context: { locale },
+ appDir: isAppDir,
+ fetchData: value,
+ options,
+ resource: fieldTypeDmmf as ModelName,
+ });
+ }
+ }
+ }
+ });
+ data = transformData(data, resource, edit ?? {}, options);
+ return data;
};
diff --git a/packages/next-admin/src/utils/props.ts b/packages/next-admin/src/utils/props.ts
index e63aabb0..04e0a087 100644
--- a/packages/next-admin/src/utils/props.ts
+++ b/packages/next-admin/src/utils/props.ts
@@ -1,26 +1,17 @@
-import { Prisma, PrismaClient } from "@prisma/client";
+import { Prisma } from "@prisma/client";
import { cloneDeep } from "lodash";
-import type { SearchPaginatedResourceParams } from "../actions";
import {
- ActionParams,
AdminComponentProps,
EditOptions,
- Field,
- ListOptions,
+ GetMainLayoutPropsParams,
+ GetNextAdminPropsParams,
MainLayoutProps,
ModelIcon,
ModelName,
NextAdminOptions,
- SubmitFormResult,
} from "../types";
-import { createBoundServerAction } from "./actions";
import { getCustomInputs } from "./options";
-import {
- getMappedDataList,
- mapDataList,
- includeOrderByPayloadForModel,
- selectPayloadForModel,
-} from "./prisma";
+import { getDataItem, getMappedDataList } from "./prisma";
import {
getModelIdProperty,
getPrismaModelForResource,
@@ -28,38 +19,10 @@ import {
getResourceIdFromParam,
getResources,
getToStringForModel,
- transformData,
transformSchema,
} from "./server";
import { extractSerializable } from "./tools";
-export type GetPropsFromParamsParams = {
- params?: string[];
- searchParams: { [key: string]: string | string[] | undefined } | undefined;
- options: NextAdminOptions;
- schema: any;
- prisma: PrismaClient;
- action?: (
- params: ActionParams,
- formData: FormData
- ) => Promise;
- isAppDir?: boolean;
- deleteAction?: (
- resource: ModelName,
- ids: string[] | number[]
- ) => Promise;
- locale?: string;
- getMessages?: () => Promise>;
- searchPaginatedResourceAction?: (
- actionBaseParams: ActionParams,
- params: SearchPaginatedResourceParams
- ) => Promise<{
- data: any[];
- total: number;
- error: string | null;
- }>;
-};
-
enum Page {
LIST = 1,
EDIT = 2,
@@ -71,19 +34,19 @@ export async function getPropsFromParams({
options,
schema,
prisma,
- action,
- isAppDir = false,
- deleteAction,
+ isAppDir = true,
locale,
getMessages,
- searchPaginatedResourceAction,
-}: GetPropsFromParamsParams): Promise<
+ basePath,
+ apiBasePath,
+}: GetNextAdminPropsParams): Promise<
| AdminComponentProps
| Omit
| Pick<
AdminComponentProps,
| "pageComponent"
| "basePath"
+ | "apiBasePath"
| "isAppDir"
| "message"
| "resources"
@@ -94,13 +57,12 @@ export async function getPropsFromParams({
resource,
resources,
resourcesTitles,
- basePath,
customPages,
title,
sidebar,
resourcesIcons,
externalLinks,
- } = getMainLayoutProps({ options, params, isAppDir });
+ } = getMainLayoutProps({ basePath, apiBasePath, options, params, isAppDir });
const resourcesIdProperty = resources!.reduce(
(acc, resource) => {
@@ -110,51 +72,36 @@ export async function getPropsFromParams({
{} as Record
);
- if (isAppDir && !action) {
- throw new Error("action is required when using App router");
- }
-
- if (isAppDir && !deleteAction) {
- throw new Error("deleteAction must be provided");
- }
-
- if (isAppDir && !searchPaginatedResourceAction) {
- throw new Error("searchPaginatedResourceAction must be provided");
- }
-
- const clientOptions: NextAdminOptions = extractSerializable(options);
+ const clientOptions: NextAdminOptions | undefined =
+ extractSerializable(options);
let defaultProps: AdminComponentProps = {
resources,
basePath,
+ apiBasePath,
isAppDir,
- action: action
- ? createBoundServerAction({ schema, params }, action)
- : undefined,
customPages,
resourcesTitles,
resourcesIdProperty,
- deleteAction,
options: clientOptions,
- searchPaginatedResourceAction: searchPaginatedResourceAction
- ? createBoundServerAction(
- { schema, params },
- searchPaginatedResourceAction
- )
- : undefined,
title,
sidebar,
resourcesIcons,
externalLinks,
+ locale,
};
if (!params) return defaultProps;
if (!resource) return defaultProps;
- const actions = options?.model?.[resource]?.actions;
+ // We don't need to pass the action function to the component
+ const actions = options?.model?.[resource]?.actions?.map((action) => {
+ const { action: _, ...actionRest } = action;
+ return actionRest;
+ });
if (getMessages) {
- const messages = await getMessages();
+ const messages = await getMessages(locale!);
const dottedProperty = {} as any;
const dot = (obj: object, prefix = "") => {
Object.entries(obj).forEach(([key, value]) => {
@@ -197,7 +144,6 @@ export async function getPropsFromParams({
}
case Page.EDIT: {
const resourceId = getResourceIdFromParam(params[1], resource);
- const idProperty = getModelIdProperty(resource);
const dmmfSchema = getPrismaModelForResource(resource);
const edit = options?.model?.[resource]?.edit as EditOptions<
@@ -214,65 +160,13 @@ export async function getPropsFromParams({
: undefined;
if (resourceId !== undefined) {
- const select = selectPayloadForModel(resource, edit, "object");
-
- Object.entries(select).forEach(([key, value]) => {
- const fieldTypeDmmf = dmmfSchema?.fields.find(
- (field) => field.name === key
- )?.type;
-
- if (fieldTypeDmmf && dmmfSchema) {
- const relatedResourceOptions =
- options.model?.[fieldTypeDmmf as ModelName]?.list;
-
- if (
- // @ts-expect-error
- edit?.fields?.[key as Field]?.display === "table"
- ) {
- if (!relatedResourceOptions?.display) {
- throw new Error(
- `'table' display mode set for field '${key}', but no list display is setup for model ${fieldTypeDmmf}`
- );
- }
-
- select[key] = {
- select: selectPayloadForModel(
- fieldTypeDmmf as ModelName,
- relatedResourceOptions as ListOptions,
- "object"
- ),
- };
- }
- }
- });
-
- // @ts-expect-error
- let data = await prisma[resource].findUniqueOrThrow({
- select,
- where: { [idProperty]: resourceId },
- });
-
- Object.entries(data).forEach(([key, value]) => {
- if (Array.isArray(value)) {
- const fieldTypeDmmf = dmmfSchema?.fields.find(
- (field) => field.name === key
- )?.type;
-
- if (fieldTypeDmmf && dmmfSchema) {
- if (
- // @ts-expect-error
- edit?.fields?.[key as Field]?.display === "table"
- ) {
- data[key] = mapDataList({
- context: { locale },
- appDir: isAppDir,
- fetchData: value,
- options,
- resource: fieldTypeDmmf as ModelName,
- });
- }
- }
- }
+ const data = await getDataItem({
+ prisma,
+ resource,
+ resourceId,
+ options,
+ locale,
+ isAppDir,
});
const toStringFunction = getToStringForModel(
@@ -281,7 +175,7 @@ export async function getPropsFromParams({
const slug = toStringFunction
? toStringFunction(data)
: resourceId.toString();
- data = transformData(data, resource, edit, options);
+
return {
...defaultProps,
resource,
@@ -310,30 +204,41 @@ export async function getPropsFromParams({
}
}
-type GetMainLayoutPropsParams = {
- options: NextAdminOptions;
- params?: string[];
- isAppDir?: boolean;
-};
-
export const getMainLayoutProps = ({
+ basePath,
+ apiBasePath,
options,
params,
- isAppDir = false,
+ isAppDir = true,
+
}: GetMainLayoutPropsParams): MainLayoutProps => {
+ if (params !== undefined && !Array.isArray(params)) {
+ throw new Error(
+ "`params` parameter in `getMainLayoutProps` should be an array of strings."
+ );
+ }
+
const resources = getResources(options);
const resource = getResourceFromParams(params ?? [], resources);
+ const dmmfSchema = getPrismaModelForResource(resource!);
+ const resourcesIdProperty = resources!.reduce(
+ (acc, resource) => {
+ acc[resource] = getModelIdProperty(resource);
+ return acc;
+ },
+ {} as Record
+ );
- const customPages = Object.keys(options.pages ?? {}).map((path) => ({
- title: options.pages![path as keyof typeof options.pages].title,
+ const customPages = Object.keys(options?.pages ?? {}).map((path) => ({
+ title: options?.pages![path as keyof typeof options.pages].title ?? path,
path: path,
- icon: options.pages![path as keyof typeof options.pages].icon,
+ icon: options?.pages![path as keyof typeof options.pages].icon,
}));
const resourcesTitles = resources.reduce(
(acc, resource) => {
acc[resource as Prisma.ModelName] =
- options.model?.[resource as keyof typeof options.model]?.title ??
+ options?.model?.[resource as keyof typeof options.model]?.title ??
resource;
return acc;
},
@@ -342,7 +247,7 @@ export const getMainLayoutProps = ({
const resourcesIcons = resources.reduce(
(acc, resource) => {
- if (!options.model?.[resource as keyof typeof options.model]?.icon)
+ if (!options?.model?.[resource as keyof typeof options.model]?.icon)
return acc;
acc[resource as Prisma.ModelName] =
options.model?.[resource as keyof typeof options.model]?.icon!;
@@ -354,14 +259,17 @@ export const getMainLayoutProps = ({
return {
resources,
resource,
- basePath: options.basePath,
+ basePath,
+ apiBasePath,
customPages,
resourcesTitles,
isAppDir,
- title: options.title ?? "Admin",
- sidebar: options.sidebar,
+ title: options?.title ?? "Admin",
+ sidebar: options?.sidebar,
resourcesIcons,
- externalLinks: options.externalLinks,
+ externalLinks: options?.externalLinks,
options: extractSerializable(options),
+ dmmfSchema: dmmfSchema?.fields,
+ resourcesIdProperty: resourcesIdProperty,
};
};
diff --git a/packages/next-admin/src/utils/server.test.ts b/packages/next-admin/src/utils/server.test.ts
index ac8aceda..b2defc6c 100644
--- a/packages/next-admin/src/utils/server.test.ts
+++ b/packages/next-admin/src/utils/server.test.ts
@@ -1,26 +1,10 @@
import {
getParamsFromUrl,
getResourceFromParams,
- getResourceFromUrl,
getResourceIdFromParam,
- getResourceIdFromUrl,
} from "./server";
describe("Server utils", () => {
- describe("getResourceFromUrl", () => {
- it("should return a resource with /admin/User", () => {
- expect(getResourceFromUrl("/admin/User", ["User"])).toEqual("User");
- });
-
- it("should return a resource with /admin/User/1", () => {
- expect(getResourceFromUrl("/admin/User/1", ["User"])).toEqual("User");
- });
-
- it("should not return a resource with /admin/Post", () => {
- expect(getResourceFromUrl("/admin/Post", ["User"])).toEqual(undefined);
- });
- });
-
describe("getResourceFromParams", () => {
it("should return a resource with /admin/User", () => {
expect(getResourceFromParams(["User"], ["User"])).toEqual("User");
@@ -39,24 +23,6 @@ describe("Server utils", () => {
});
});
- describe("getResourceIdFromUrl", () => {
- it("should get the id from /admin/User/1", () => {
- expect(getResourceIdFromUrl("/admin/User/1", "User")).toEqual(1);
- });
-
- it("should not return an id from /admin/User/new", () => {
- expect(getResourceIdFromUrl("/admin/User/new", "User")).toEqual(
- undefined
- );
- });
-
- it("should not return an id from /admin/Dummy/--__", () => {
- expect(getResourceIdFromUrl("/admin/Dummy/--__", "User")).toEqual(
- undefined
- );
- });
- });
-
describe("getResourceIdFromParam", () => {
it("should get the id from /admin/User/1", () => {
expect(getResourceIdFromParam("1", "User")).toEqual(1);
diff --git a/packages/next-admin/src/utils/server.ts b/packages/next-admin/src/utils/server.ts
index 8180f38a..b32b7994 100644
--- a/packages/next-admin/src/utils/server.ts
+++ b/packages/next-admin/src/utils/server.ts
@@ -1,6 +1,7 @@
import { Prisma } from "@prisma/client";
import formidable from "formidable";
import { IncomingMessage } from "http";
+import { NextApiRequest } from "next";
import { Writable } from "node:stream";
import {
AdminFormData,
@@ -24,7 +25,7 @@ export const models: readonly Prisma.DMMF.Model[] = Prisma.dmmf.datamodel
export const enums = Prisma.dmmf.datamodel.enums;
export const resources = models.map((model) => model.name as ModelName);
-export const getEnumValues = (enumName: string) => {
+const getEnumValues = (enumName: string) => {
const enumValues = enums.find((en) => en.name === enumName);
return enumValues?.values;
};
@@ -50,7 +51,7 @@ export const getModelIdProperty = (model: ModelName) => {
return idField?.name ?? "id";
};
-export const getDeepRelationModel = (
+const getDeepRelationModel = (
model: M,
property: Field
): Prisma.DMMF.Field | undefined => {
@@ -121,7 +122,7 @@ export const getToStringForModel = (
*
* @returns schema
*/
-export const orderSchema =
+const orderSchema =
(resource: ModelName, options?: NextAdminOptions) => (schema: Schema) => {
const modelName = resource;
const model = models.find((model) => model.name === modelName);
@@ -168,7 +169,7 @@ export const fillRelationInSchema =
let fields;
if (model?.fields && display) {
// @ts-expect-error
- fields = model.fields.filter((field) => display.includes(field.name));
+ fields = model.fields?.filter((field) => display.includes(field.name));
} else {
fields = model?.fields;
}
@@ -258,14 +259,14 @@ export const transformData = (
data: any,
resource: M,
editOptions: EditOptions,
- options: NextAdminOptions
+ options?: NextAdminOptions
) => {
const modelName = resource;
const model = models.find((model) => model.name === modelName);
if (!model) return data;
return Object.keys(data).reduce((acc, key) => {
- const field = model.fields.find((field) => field.name === key);
+ const field = model.fields?.find((field) => field.name === key);
const fieldKind = field?.kind;
const get = editOptions?.fields?.[key as Field]?.handler?.get;
const explicitManyToManyRelationField =
@@ -475,7 +476,7 @@ export const formatId = (resource: ModelName, id: string) => {
const model = models.find((model) => model.name === resource);
const idProperty = getModelIdProperty(resource);
- return model?.fields.find((field) => field.name === idProperty)?.type ===
+ return model?.fields?.find((field) => field.name === idProperty)?.type ===
"Int"
? Number(id)
: id;
@@ -739,8 +740,9 @@ export const formattedFormData = async (
formattedData[dmmfPropertyName] =
formData[dmmfPropertyName] === "on";
} else if (dmmfPropertyType === "DateTime") {
- formattedData[dmmfPropertyName] =
- formData[dmmfPropertyName] || null;
+ formattedData[dmmfPropertyName] = formData[dmmfPropertyName]
+ ? new Date(formData[dmmfPropertyName]!)
+ : null;
} else if (dmmfPropertyType === "Json") {
try {
formattedData[dmmfPropertyName] = formData[dmmfPropertyName]
@@ -852,7 +854,7 @@ export const transformSchema = (
orderSchema(resource, options)
);
-export const fillDescriptionInSchema = (
+const fillDescriptionInSchema = (
resource: M,
editOptions: EditOptions
) => {
@@ -920,13 +922,6 @@ export const removeHiddenProperties =
return schema;
};
-export const getResourceFromUrl = (
- url: string,
- resources: Prisma.ModelName[]
-): ModelName | undefined => {
- return resources.find((r) => url.includes(`/${r}`));
-};
-
export const getResourceFromParams = (
params: string[],
resources: Prisma.ModelName[]
@@ -961,26 +956,6 @@ export const getParamsFromUrl = (url: string, basePath: string) => {
return urlWithoutParams.split("/").filter(Boolean);
};
-export const getResourceIdFromUrl = (
- url: string,
- resource: ModelName
-): string | number | undefined => {
- const matching = url.match(`/${resource}/([0-9a-z-]+)`);
-
- if (!matching) return undefined;
- if (matching[1] === "new") return undefined;
-
- const model = models.find((model) => model.name === resource);
-
- const idType = model?.fields.find((field) => field.name === "id")?.type;
-
- if (idType === "Int") {
- return Number(matching[1]);
- }
-
- return matching ? matching[1] : undefined;
-};
-
export const getResourceIdFromParam = (param: string, resource: ModelName) => {
if (param === "new") {
return undefined;
@@ -1020,10 +995,15 @@ export const getFormDataValues = async (req: IncomingMessage) => {
if (!file.originalFilename) {
files[name] = [null];
} else {
- files[name] = [[Buffer.concat(chunks), {
- name: file.originalFilename,
- type: file.mimetype,
- }]];
+ files[name] = [
+ [
+ Buffer.concat(chunks),
+ {
+ name: file.originalFilename,
+ type: file.mimetype,
+ },
+ ],
+ ];
}
callback();
},
@@ -1070,10 +1050,13 @@ export const getFormValuesFromFormData = async (formData: FormData) => {
return;
}
const buffer = await file.arrayBuffer();
- formValues[key] = [Buffer.from(buffer), {
- name: file.name,
- type: file.type,
- }];
+ formValues[key] = [
+ Buffer.from(buffer),
+ {
+ name: file.name,
+ type: file.type,
+ },
+ ];
} else {
formValues[key] = value as string;
}
@@ -1083,6 +1066,21 @@ export const getFormValuesFromFormData = async (formData: FormData) => {
return formValues;
};
+export const getJsonBody = async (req: NextApiRequest): Promise => {
+ let body = await getBody(req);
+
+ // Handle case where bodyParser is disabled
+ if (
+ body &&
+ typeof body === "string" &&
+ req.headers["content-type"] === "application/json"
+ ) {
+ body = JSON.parse(body);
+ }
+
+ return body;
+};
+
export const getBody = async (req: IncomingMessage) => {
return new Promise((resolve) => {
const bodyParts: Buffer[] = [];
diff --git a/packages/next-admin/src/utils/tools.ts b/packages/next-admin/src/utils/tools.ts
index d14c03da..e31919ce 100644
--- a/packages/next-admin/src/utils/tools.ts
+++ b/packages/next-admin/src/utils/tools.ts
@@ -1,3 +1,4 @@
+import React from "react";
import { UploadParameters } from "../types";
export const capitalize = (str: T): Capitalize => {
@@ -69,10 +70,27 @@ export const formatLabel = (label: string) => {
//Create a function that check if object satifies UploadParameters
export const isUploadParameters = (obj: any): obj is UploadParameters => {
return (
- obj.length === 2 &&
+ obj?.length === 2 &&
Buffer.isBuffer(obj[0]) &&
- typeof obj[1] === 'object' &&
- 'name' in obj[1] &&
- 'type' in obj[1]
+ typeof obj[1] === "object" &&
+ "name" in obj[1] &&
+ "type" in obj[1]
);
-}
\ No newline at end of file
+};
+
+export const getDisplayedValue = (
+ element: React.ReactElement | string
+): string => {
+ if (typeof element === "string") {
+ return element;
+ } else if (React.isValidElement(element)) {
+ return Array.prototype.map
+ .call(
+ (element.props as any).children,
+ (child: React.ReactElement | string) => getDisplayedValue(child)
+ )
+ .join("");
+ } else {
+ return "";
+ }
+};
diff --git a/packages/next-admin/tsconfig.json b/packages/next-admin/tsconfig.json
index d0072ac5..83d22edf 100644
--- a/packages/next-admin/tsconfig.json
+++ b/packages/next-admin/tsconfig.json
@@ -1,13 +1,14 @@
{
"extends": "tsconfig/react-library.json",
"include": [
+ "./src/pageRouter.ts",
+ "src/index.ts",
"./src/appRouter.ts",
- "./src/index.tsx",
- "./src/router.tsx",
"./src/actions/index.ts",
- "./src/mainLayout.tsx",
"./src/plugin.ts",
- "./src/preset.ts"
+ "./src/preset.ts",
+ "./src/appHandler.ts",
+ "./src/pageHandler.ts"
],
"exclude": ["dist", "build", "node_modules"],
"compilerOptions": {
diff --git a/turbo.json b/turbo.json
index 79817605..d3d3d5d3 100644
--- a/turbo.json
+++ b/turbo.json
@@ -1,7 +1,9 @@
{
"$schema": "https://turbo.build/schema.json",
- "globalDependencies": ["**/.env.*local"],
- "globalDotEnv": ["**/.env"],
+ "globalDependencies": [
+ "**/.env.*local",
+ "**/.env"
+ ],
"globalEnv": [
"NODE_ENV",
"BASE_URL",
@@ -9,11 +11,17 @@
"POSTGRES_PRISMA_URL",
"POSTGRES_URL_NON_POOLING"
],
- "pipeline": {
+ "tasks": {
"start": {},
"build": {
- "dependsOn": ["^build"],
- "outputs": ["dist/**", ".next/**", "!.next/cache/**"]
+ "dependsOn": [
+ "^build"
+ ],
+ "outputs": [
+ "dist/**",
+ ".next/**",
+ "!.next/cache/**"
+ ]
},
"lint": {
"outputs": []
diff --git a/yarn.lock b/yarn.lock
index 85666b32..eeb9d51d 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1553,7 +1553,7 @@
resolved "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz"
integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==
-"@prisma/client@^5.13.0":
+"@prisma/client@5.14.0":
version "5.14.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-5.14.0.tgz#dadca5bb1137ddcebb454bbdaf89423823d3363f"
integrity sha512-akMSuyvLKeoU4LeyBAUdThP/uhVP3GuLygFE3MlYzaCb3/J8SfsYBE5PkaFuLuVpLyA6sFoW+16z/aPhNAESqg==
@@ -2064,37 +2064,37 @@
dependencies:
"@babel/runtime" "^7.13.10"
-"@rjsf/core@^5.3.0":
- version "5.3.1"
- resolved "https://registry.npmjs.org/@rjsf/core/-/core-5.3.1.tgz"
- integrity sha512-AejBNI5XBmfirubZ2L/f7Hx9hga4E3w777UB4ZvzAPwNygxKS3p/4nUKuP4Ho53aktsArdva0mdmgdBNn8phCw==
+"@rjsf/core@^5.19.3":
+ version "5.19.3"
+ resolved "https://registry.yarnpkg.com/@rjsf/core/-/core-5.19.3.tgz#34d476f0ec9d25fd838e374c861acb9ee9860c25"
+ integrity sha512-AAzolj+fcFlwk0/5THA7T2JkmYTIUa+fLPM5prFqPw55FwWOa0qC5zIOfkhPS95Z9bfwJv3BubOAiKZ7MOGe8Q==
dependencies:
- lodash "^4.17.15"
- lodash-es "^4.17.15"
- markdown-to-jsx "^7.1.9"
- nanoid "^3.3.4"
- prop-types "^15.7.2"
+ lodash "^4.17.21"
+ lodash-es "^4.17.21"
+ markdown-to-jsx "^7.4.1"
+ nanoid "^3.3.7"
+ prop-types "^15.8.1"
-"@rjsf/utils@^5.3.0":
- version "5.3.1"
- resolved "https://registry.npmjs.org/@rjsf/utils/-/utils-5.3.1.tgz"
- integrity sha512-bpqq1c8nivNaynGgQa/ZOTxZaD8RYo1YYF/cHpmTqGGO+qwNhimJdFCxu6uOJ5dRP59nsJ3nYbhzK1cLtxFXfQ==
+"@rjsf/utils@^5.19.3":
+ version "5.19.3"
+ resolved "https://registry.yarnpkg.com/@rjsf/utils/-/utils-5.19.3.tgz#776ae78610af61a7e1de1149e158e0bc4a1e45bd"
+ integrity sha512-1lG/uMMmnAJE48BHUl4laNY2W2j2gIR2LH4jsxeEMSuFloB06ZuUXLesD03Nz2zQjm66izNmnm5eAmAi3Pa1yQ==
dependencies:
json-schema-merge-allof "^0.8.1"
jsonpointer "^5.0.1"
- lodash "^4.17.15"
- lodash-es "^4.17.15"
+ lodash "^4.17.21"
+ lodash-es "^4.17.21"
react-is "^18.2.0"
-"@rjsf/validator-ajv8@^5.3.0":
- version "5.3.1"
- resolved "https://registry.npmjs.org/@rjsf/validator-ajv8/-/validator-ajv8-5.3.1.tgz"
- integrity sha512-nl0mcUqbCZWZsjrPy7n0rdJtc0QprIl35rfu6YltCQxFuiCpVzl3CVo99grUg9kO5X6i4be8JbXkuUMaE73DXg==
+"@rjsf/validator-ajv8@^5.19.3":
+ version "5.19.3"
+ resolved "https://registry.yarnpkg.com/@rjsf/validator-ajv8/-/validator-ajv8-5.19.3.tgz#924b12f4ada82b9750dfdb3853e6c4b98afce579"
+ integrity sha512-ah0DY1tGmBDNd0iIjH9/iL3TSflriY8cYPmAwK4aDzGLnwHgD2w8sEfJJjG01pX89rY2CeeYxcwCalwlBlUZ/w==
dependencies:
ajv "^8.12.0"
ajv-formats "^2.1.1"
- lodash "^4.17.15"
- lodash-es "^4.17.15"
+ lodash "^4.17.21"
+ lodash-es "^4.17.21"
"@rushstack/eslint-patch@^1.1.3":
version "1.2.0"
@@ -2229,6 +2229,11 @@
resolved "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz"
integrity sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==
+"@tsconfig/node16@^1.0.3":
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9"
+ integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==
+
"@types/acorn@^4.0.0":
version "4.0.6"
resolved "https://registry.npmjs.org/@types/acorn/-/acorn-4.0.6.tgz"
@@ -6568,7 +6573,7 @@ locate-path@^6.0.0:
dependencies:
p-locate "^5.0.0"
-lodash-es@^4.17.15, lodash-es@^4.17.21:
+lodash-es@^4.17.21:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
@@ -6613,7 +6618,7 @@ lodash.truncate@^4.4.2:
resolved "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz"
integrity sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==
-lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
+lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.21, lodash@^4.17.4:
version "4.17.21"
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
@@ -6696,10 +6701,10 @@ markdown-table@^3.0.0:
resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz"
integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==
-markdown-to-jsx@^7.1.9:
- version "7.2.0"
- resolved "https://registry.npmjs.org/markdown-to-jsx/-/markdown-to-jsx-7.2.0.tgz"
- integrity sha512-3l4/Bigjm4bEqjCR6Xr+d4DtM1X6vvtGsMGSjJYyep8RjjIvcWtrXBS8Wbfe1/P+atKNMccpsraESIaWVplzVg==
+markdown-to-jsx@^7.4.1:
+ version "7.4.7"
+ resolved "https://registry.yarnpkg.com/markdown-to-jsx/-/markdown-to-jsx-7.4.7.tgz#740ee7ec933865ef5cc683a0992797685a75e2ee"
+ integrity sha512-0+ls1IQZdU6cwM1yu0ZjjiVWYtkbExSyUIFU2ZeDIFuZM1W42Mh4OlJ4nb4apX4H8smxDHRdFaoIVJGwfv5hkg==
match-sorter@^6.3.1:
version "6.3.1"
@@ -7507,10 +7512,11 @@ negotiator@^0.6.3:
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
next-connect@^1.0.0-next.3:
- version "1.0.0-next.3"
- resolved "https://registry.npmjs.org/next-connect/-/next-connect-1.0.0-next.3.tgz"
- integrity sha512-i1kb8rz/3lm6z68Lnh18juHGgbgFVZXXAIiElaASGXxDZ8mJ2EVxdbTXX8NiF9BbGDBeao7u6uKMqw5ZLZg/Kg==
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/next-connect/-/next-connect-1.0.0.tgz#49361a92b2d22db1cce73f94dfe793cd4b9e0106"
+ integrity sha512-FeLURm9MdvzY1SDUGE74tk66mukSqL6MAzxajW7Gqh6DZKBZLrXmXnGWtHJZXkfvoi+V/DUe9Hhtfkl4+nTlYA==
dependencies:
+ "@tsconfig/node16" "^1.0.3"
regexparam "^2.0.1"
next-intl@^3.3.2:
@@ -7549,7 +7555,12 @@ next-themes@^0.2.1:
resolved "https://registry.npmjs.org/next-themes/-/next-themes-0.2.1.tgz"
integrity sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==
-next@13.2.4, next@^13.1.1:
+next-themes@^0.3.0:
+ version "0.3.0"
+ resolved "https://registry.yarnpkg.com/next-themes/-/next-themes-0.3.0.tgz#b4d2a866137a67d42564b07f3a3e720e2ff3871a"
+ integrity sha512-/QHIrsYpd6Kfk7xakK4svpDI5mmXP0gfvCoJdGpZQ2TOrQZmsW0QxjaiLn8wbIKjtm4BTSqLoix4lxYYOnLJ/w==
+
+next@^13.1.1:
version "13.2.4"
resolved "https://registry.npmjs.org/next/-/next-13.2.4.tgz"
integrity sha512-g1I30317cThkEpvzfXujf0O4wtaQHtDCLhlivwlTJ885Ld+eOgcz7r3TGQzeU+cSRoNHtD8tsJgzxVdYojFssw==
@@ -8296,7 +8307,7 @@ react-day-picker@^8.7.1:
resolved "https://registry.yarnpkg.com/react-day-picker/-/react-day-picker-8.8.0.tgz#582b9d5e54a84926f159be2b4004801707b3c885"
integrity sha512-QIC3uOuyGGbtypbd5QEggsCSqVaPNu8kzUWquZ7JjW9fuWB9yv7WyixKmnaFelTLXFdq7h7zU6n/aBleBqe/dA==
-react-dom@18.2.0, react-dom@^18.2.0:
+react-dom@^18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
@@ -8405,7 +8416,7 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0"
prop-types "^15.6.2"
-react@18.2.0, react@^18.2.0:
+react@^18.2.0:
version "18.2.0"
resolved "https://registry.npmjs.org/react/-/react-18.2.0.tgz"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
@@ -9595,47 +9606,47 @@ tty-table@^4.1.5:
wcwidth "^1.0.1"
yargs "^17.7.1"
-turbo-darwin-64@1.10.3:
- version "1.10.3"
- resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-1.10.3.tgz#b04f715530ae3c8b6d1ce86229236a7513a28c8c"
- integrity sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==
+turbo-darwin-64@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/turbo-darwin-64/-/turbo-darwin-64-2.0.4.tgz#83c7835f8ba1f7a5473487ce73cfc8d5ad523614"
+ integrity sha512-x9mvmh4wudBstML8Z8IOmokLWglIhSfhQwnh2gBCSqabgVBKYvzl8Y+i+UCNPxheCGTgtsPepTcIaKBIyFIcvw==
-turbo-darwin-arm64@1.10.3:
- version "1.10.3"
- resolved "https://registry.npmjs.org/turbo-darwin-arm64/-/turbo-darwin-arm64-1.10.3.tgz"
- integrity sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==
+turbo-darwin-arm64@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/turbo-darwin-arm64/-/turbo-darwin-arm64-2.0.4.tgz#046e5768e9d6b490b7108d5bef3f4a1594aca0ba"
+ integrity sha512-/B1Ih8zPRGVw5vw4SlclOf3C/woJ/2T6ieH6u54KT4wypoaVyaiyMqBcziIXycdObIYr7jQ+raHO7q3mhay9/A==
-turbo-linux-64@1.10.3:
- version "1.10.3"
- resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-1.10.3.tgz#d6cbd198e95620e75baa70f1e09f355db6d3e1de"
- integrity sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==
+turbo-linux-64@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/turbo-linux-64/-/turbo-linux-64-2.0.4.tgz#eab8c183a11b26ddec251d62778313a495971e4f"
+ integrity sha512-6aG670e5zOWu6RczEYcB81nEl8EhiGJEvWhUrnAfNEUIMBEH1pR5SsMmG2ol5/m3PgiRM12r13dSqTxCLcHrVg==
-turbo-linux-arm64@1.10.3:
- version "1.10.3"
- resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-1.10.3.tgz#53148b79e84d020ece82c8af170a2f1d16a01b5b"
- integrity sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==
+turbo-linux-arm64@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/turbo-linux-arm64/-/turbo-linux-arm64-2.0.4.tgz#2dcc3f1d3e56209736b2ce3d849b80e0d7116e42"
+ integrity sha512-AXfVOjst+mCtPDFT4tCu08Qrfv12Nj7NDd33AjGwV79NYN1Y1rcFY59UQ4nO3ij3rbcvV71Xc+TZJ4csEvRCSg==
-turbo-windows-64@1.10.3:
- version "1.10.3"
- resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-1.10.3.tgz#a90af7313cbada57296d672515c4957ef86e5905"
- integrity sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==
+turbo-windows-64@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/turbo-windows-64/-/turbo-windows-64-2.0.4.tgz#b2440d82892c983088ed386f9126d365594fc1a5"
+ integrity sha512-QOnUR9hKl0T5gq5h1fAhVEqBSjpcBi/BbaO71YGQNgsr6pAnCQdbG8/r3MYXet53efM0KTdOhieWeO3KLNKybA==
-turbo-windows-arm64@1.10.3:
- version "1.10.3"
- resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-1.10.3.tgz#3ed80e34aa5a432b312ccf2f4770c63a72d0b254"
- integrity sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==
+turbo-windows-arm64@2.0.4:
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/turbo-windows-arm64/-/turbo-windows-arm64-2.0.4.tgz#e943709535baf233f5b85ed35cd95dcf86815283"
+ integrity sha512-3v8WpdZy1AxZw0gha0q3caZmm+0gveBQ40OspD6mxDBIS+oBtO5CkxhIXkFJJW+jDKmDlM7wXDIGfMEq+QyNCQ==
turbo@latest:
- version "1.10.3"
- resolved "https://registry.yarnpkg.com/turbo/-/turbo-1.10.3.tgz#125944cb59f3aa60ca4aa93e4c505b974fe55097"
- integrity sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==
+ version "2.0.4"
+ resolved "https://registry.yarnpkg.com/turbo/-/turbo-2.0.4.tgz#4fb6f0bf3be905953825de0368203e849c91e412"
+ integrity sha512-Ilme/2Q5kYw0AeRr+aw3s02+WrEYaY7U8vPnqSZU/jaDG/qd6jHVN6nRWyd/9KXvJGYM69vE6JImoGoyNjLwaw==
optionalDependencies:
- turbo-darwin-64 "1.10.3"
- turbo-darwin-arm64 "1.10.3"
- turbo-linux-64 "1.10.3"
- turbo-linux-arm64 "1.10.3"
- turbo-windows-64 "1.10.3"
- turbo-windows-arm64 "1.10.3"
+ turbo-darwin-64 "2.0.4"
+ turbo-darwin-arm64 "2.0.4"
+ turbo-linux-64 "2.0.4"
+ turbo-linux-arm64 "2.0.4"
+ turbo-windows-64 "2.0.4"
+ turbo-windows-arm64 "2.0.4"
type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0"