diff --git a/.changeset/big-bobcats-occur.md b/.changeset/big-bobcats-occur.md new file mode 100644 index 00000000..4bc6b6f2 --- /dev/null +++ b/.changeset/big-bobcats-occur.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Add `next-themes` to handle color scheme diff --git a/.changeset/chilled-llamas-grab.md b/.changeset/chilled-llamas-grab.md new file mode 100644 index 00000000..707e1b1f --- /dev/null +++ b/.changeset/chilled-llamas-grab.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Redirect useEffect diff --git a/.changeset/flat-pandas-dance.md b/.changeset/flat-pandas-dance.md new file mode 100644 index 00000000..63bdb42b --- /dev/null +++ b/.changeset/flat-pandas-dance.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +add dist diff --git a/.changeset/gentle-cooks-tickle.md b/.changeset/gentle-cooks-tickle.md new file mode 100644 index 00000000..32c5e2a1 --- /dev/null +++ b/.changeset/gentle-cooks-tickle.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Change logout system (Request or server action) diff --git a/.changeset/great-bees-pump.md b/.changeset/great-bees-pump.md new file mode 100644 index 00000000..2ea95415 --- /dev/null +++ b/.changeset/great-bees-pump.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Fix images CORS issues diff --git a/.changeset/pre.json b/.changeset/pre.json new file mode 100644 index 00000000..6f73f065 --- /dev/null +++ b/.changeset/pre.json @@ -0,0 +1,26 @@ +{ + "mode": "exit", + "tag": "rc", + "initialVersions": { + "docs": "0.0.0", + "example": "0.0.0", + "eslint-config-custom": "0.0.0", + "@premieroctet/next-admin": "4.4.5", + "tsconfig": "0.0.0" + }, + "changesets": [ + "big-bobcats-occur", + "chilled-llamas-grab", + "flat-pandas-dance", + "gentle-cooks-tickle", + "great-bees-pump", + "quiet-otters-study", + "spotty-forks-greet", + "stale-cycles-peel", + "strong-cobras-look", + "tricky-brooms-appear", + "weak-olives-call", + "young-maps-study", + "young-ties-care" + ] +} diff --git a/.changeset/quiet-otters-study.md b/.changeset/quiet-otters-study.md new file mode 100644 index 00000000..8d5371fd --- /dev/null +++ b/.changeset/quiet-otters-study.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Small fixes (select, dark mode, dashboard, layout, doc) diff --git a/.changeset/spotty-forks-greet.md b/.changeset/spotty-forks-greet.md new file mode 100644 index 00000000..ec074d44 --- /dev/null +++ b/.changeset/spotty-forks-greet.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Add history on redirect `Save` diff --git a/.changeset/stale-cycles-peel.md b/.changeset/stale-cycles-peel.md new file mode 100644 index 00000000..9c30bddc --- /dev/null +++ b/.changeset/stale-cycles-peel.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Fix date input and add time-second format diff --git a/.changeset/strong-cobras-look.md b/.changeset/strong-cobras-look.md new file mode 100644 index 00000000..54b799e4 --- /dev/null +++ b/.changeset/strong-cobras-look.md @@ -0,0 +1,128 @@ +--- +"@premieroctet/next-admin": major +--- + +## Major Changes + +- **Breaking Change**: + + - New implementation of `NextAdmin`. Usage of `API route` instead of `server actions`. + - Configuration of `page.tsx` and `route.ts` files in the `app/admin/[[...nextadmin]]` and `app/api/[[...nextadmin]]` folders respectively. + - `createHandler` function now available in `appHandler` and `pageHandler` modules to configure the API route. + - `getNextAdminProps` function now available in `appRouter` and `pageRouter` modules to configure the page route. + +## Migration + +### API Route `[[...nextadmin]]` + +Create a dynamic route `[[...nextadmin]]` to handle all the API routes. + +
+App router + +```tsx +// app/api/admin/[[...nextadmin]]/route.ts +import { prisma } from "@/prisma"; +import { createHandler } from "@premieroctet/next-admin/dist/appHandler"; + +const { run } = createHandler({ + apiBasePath: "/api/admin", + prisma, + /*options*/ +}); + +export { run as DELETE, run as GET, run as POST }; +``` + +
+ +
+Page router + +```ts copy + // pages/api/admin/[[...nextadmin]].ts + import { prisma } from "@/prisma"; + import { createApiRouter } from "@premieroctet/next-admin/dist/pageHandler"; + import schema from "@/prisma/json-schema/json-schema.json"; + + export const config = { + api: { + bodyParser: false, + }, + }; + + const { run } = createHandler({ + apiBasePath: "/api/admin", + prisma, + schema: schema, + /*options*/, + }); + + export default run; +``` + +
+ +### Change `getPropsFromParams` to `getNextAdminProps` + +
+App router + +Replace the `getPropsFromParams` function with the `getNextAdminProps` function in the `page.tsx` file. + +```tsx +// app/admin/[[...nextadmin]]/page.tsx +import { NextAdmin, PageProps } from "@premieroctet/next-admin"; +import { getNextAdminProps } from "@premieroctet/next-admin/dist/appRouter"; +import { prisma } from "@/prisma"; + +export default async function AdminPage({ params, searchParams }: PageProps) { + const props = await getNextAdminProps({ + params: params.nextadmin, + searchParams, + basePath: "/admin", + apiBasePath: "/api/admin", + prisma, + /*options*/ + }); + + return ; +} +``` + +
+ +
+Page router + +Do not use `nextAdminRouter` anymore. Replace it with the `getNextAdminProps` function in the `[[...nextadmin]].ts` file for `getServerSideProps`. + +```tsx copy +// pages/admin/[[...nextadmin]].tsx +import { AdminComponentProps, NextAdmin } from "@premieroctet/next-admin"; + +import { getNextAdminProps } from "@premieroctet/next-admin/dist/pageRouter"; +import { GetServerSideProps } from "next"; +import { prisma } from " @/prisma"; +import schema from "@/prisma/json-schema/json-schema.json"; +import "@/styles.css"; + +export default function Admin(props: AdminComponentProps) { + return ( + + ); +} + +export const getServerSideProps: GetServerSideProps = async ({ req }) => + await getNextAdminProps({ + basePath: "/pagerouter/admin", + apiBasePath: "/api/pagerouter/admin", + prisma, + schema, + /*options*/ + req, + }); +``` diff --git a/.changeset/tricky-brooms-appear.md b/.changeset/tricky-brooms-appear.md new file mode 100644 index 00000000..09f53dc6 --- /dev/null +++ b/.changeset/tricky-brooms-appear.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Add `isDirty` for form to submit only fields touched diff --git a/.changeset/weak-olives-call.md b/.changeset/weak-olives-call.md new file mode 100644 index 00000000..078f7290 --- /dev/null +++ b/.changeset/weak-olives-call.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Dependency `next-themes` diff --git a/.changeset/young-maps-study.md b/.changeset/young-maps-study.md new file mode 100644 index 00000000..90fde8ba --- /dev/null +++ b/.changeset/young-maps-study.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +add URL redirect support for logout diff --git a/.changeset/young-ties-care.md b/.changeset/young-ties-care.md new file mode 100644 index 00000000..e3d4380e --- /dev/null +++ b/.changeset/young-ties-care.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +Merge main branch diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 8d2aacc6..ed346b31 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -20,7 +20,7 @@ jobs: path: ~/.cache/ms-playwright key: ${{ runner.os }}-playwright-${{ hashFiles('**/yarn.lock') }} - name: Start docker-compose - run: docker-compose up -d + run: docker compose up -d - name: Install dependencies run: yarn install - name: Run linter diff --git a/apps/docs/components/OptionsTable.tsx b/apps/docs/components/OptionsTable.tsx new file mode 100644 index 00000000..516fe71d --- /dev/null +++ b/apps/docs/components/OptionsTable.tsx @@ -0,0 +1,75 @@ +interface Option { + name: string; + type?: string; + description: string | React.ReactNode; + defaultValue?: string; +} + +interface HeadersLabel { + name: string; + type: string; + description: string; + defaultValue: string; +} + +interface OptionsTableProps { + options: Option[]; + labels?: HeadersLabel; +} + +export function OptionsTable({ options, labels }: OptionsTableProps) { + const hasTypeColumn = options.some((option) => Boolean(option.type)); + const hasDefaultValueColumn = options.some((option) => + Boolean(option.defaultValue) + ); + + return ( +
+ + + + + {hasTypeColumn && ( + + )} + + {hasDefaultValueColumn && ( + + )} + + + + {options.map(({ name, type, description, defaultValue }) => ( + + + {hasTypeColumn && ( + + )} + + {hasDefaultValueColumn && ( + + )} + + ))} + +
{labels?.name || "Name"} + {labels?.type || "Type"} + + {labels?.description || "Description"} + + {labels?.defaultValue || "Default Value"} +
+ {name} + + {Boolean(type) ? type : "-"} + {description} + {Boolean(defaultValue) ? defaultValue : "-"} +
+
+ ); +} + +export default OptionsTable; diff --git a/apps/docs/package.json b/apps/docs/package.json index 48505bad..9b088e7b 100644 --- a/apps/docs/package.json +++ b/apps/docs/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@heroicons/react": "^2.1.1", - "@premieroctet/next-admin": "*", + "@premieroctet/next-admin": "5.0.0-rc.14", "clsx": "^2.1.0", "framer-motion": "^11.0.8", "mini-svg-data-uri": "^1.4.4", diff --git a/apps/docs/pages/_meta.json b/apps/docs/pages/_meta.json index 4671f7c4..d1d7d236 100644 --- a/apps/docs/pages/_meta.json +++ b/apps/docs/pages/_meta.json @@ -12,6 +12,11 @@ "type": "page", "display": "hidden" }, + "v4": { + "title": "v4", + "type": "page", + "display": "hidden" + }, "changelog": { "title": "Changelog", "type": "page" diff --git a/apps/docs/pages/docs/_meta.json b/apps/docs/pages/docs/_meta.json index 31f0e073..ff3dfbe5 100644 --- a/apps/docs/pages/docs/_meta.json +++ b/apps/docs/pages/docs/_meta.json @@ -2,7 +2,16 @@ { "index": "Introduction", "getting-started": "Getting Started", - "api-docs": "API", + "api": { + "title": "API", + "theme": { + "breadcrumb": true, + "footer": true, + "sidebar": true, + "toc": true, + "pagination": true + } + }, "i18n": "I18n", "theming": "Theming", "glossary": "Glossary", diff --git a/apps/docs/pages/docs/api-docs.mdx b/apps/docs/pages/docs/api-docs.mdx deleted file mode 100644 index 951e5875..00000000 --- a/apps/docs/pages/docs/api-docs.mdx +++ /dev/null @@ -1,485 +0,0 @@ -import { Tabs } from "nextra/components"; - -# API - -## Functions - - - - The following is used only for App router. - - ## `getPropsFromParams` function - - `getPropsFromParams` is a function that returns the props for the [`NextAdmin`](#nextadmin--component) component. It accepts one argument which is an object with the following properties: - - - `params`: the array of route params retrieved from the [optional catch-all segment](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#optional-catch-all-segments) - - `searchParams`: the query params [retrieved from the page](https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional) - - `options`: the [options](#next-admin-options) object - - `schema`: the json schema generated by the `prisma generate` command - - `prisma`: your Prisma client instance - - `action`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to submit the form. It should be your own action, that wraps the `submitForm` action imported from `@premieroctet/next-admin/dist/actions`. - - `deleteAction`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to delete one or more records in a resource. It is optional, and should be your own action. This action takes 3 parameters: `model` (the model name) and `ids` (an array of ids to delete). Next Admin provides a default action for deletion, that you can call in your own action. Check the example app for more details. - - `getMessages`: a function with no parameters that returns translation messages. It is used to translate the default messages of the library. See [i18n](/docs/i18n) for more details. - - `searchPaginatedResourceAction`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to search for resources in a selector widget. This is mandatory for App Router, and will be ignored on page router. Just like `action`, it should be your own action that wraps `searchPaginatedResource` imported from `@premieroctet/next-admin/dist/actions`. - - - - The following is used only for Page router - - ## `nextAdminRouter` function - - `nextAdminRouter` is a function that returns a promise of a _Node Router_ that you can use in your getServerSideProps function to start using Next Admin. Its usage is only related to Page router. - - Usage example: - - ```ts copy - // pages/api/admin/[[...nextadmin]].ts - export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const { nextAdminRouter } = await import( - "@premieroctet/next-admin/dist/router" - ); - const adminRouter = await nextAdminRouter(prisma, schema); - return adminRouter.run(req, res) as Promise< - GetServerSidePropsResult<{ [key: string]: any }> - >; - }; - ``` - - It takes 3 parameters: - - - Your Prisma client instance, _required_ - - Your Prisma schema, _required_ - - and an _optional_ object of type [`NextAdminOptions`](#next-admin-options) to customize your admin with the following properties: - - ```ts - import { NextAdminOptions } from "@premieroctet/next-admin"; - - const options: NextAdminOptions = { - model: { - User: { - toString: (user) => `${user.email} / ${user.name}`, - }, - }, - }; - - const adminRouter = await nextAdminRouter(prisma, schema, options); - ``` - - - - -## Authentication - - - - The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check in the page: - - > The following example uses [next-auth](https://next-auth.js.org/) to handle authentication - - ```ts - // app/admin/[[...nextadmin]]/page.tsx - - export default async function AdminPage({ - params, - searchParams, - }: { - params: { [key: string]: string[] }; - searchParams: { [key: string]: string | string[] | undefined } | undefined; - }) { - const session = await getServerSession(authOptions); - const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check - - if (!isAdmin) { - redirect('/', { permanent: false }) - } - - const props = await getPropsFromParams({ - params: params.nextadmin, - searchParams, - options, - prisma, - schema, - action: submitFormAction, - }); - - return ; - } - ``` - - - - The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check to the `getServerSideProps` function: - - > The following example uses [next-auth](https://next-auth.js.org/) to handle authentication - - ```ts - // pages/api/admin/[[...nextadmin]].ts - - export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const session = await getServerSession(req, res, authOptions); - const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check - - if (!isAdmin) { - return { - redirect: { - destination: "/", - permanent: false, - }, - }; - } - - const { nextAdminRouter } = await import( - "@premieroctet/next-admin/dist/nextAdminRouter" - ); - return nextAdminRouter(client).run(req, res); - }; - ``` - - - - -## `` component - -`` is a React component that contains the entire UI of Next Admin. It can take several props: - -- `AdminComponentProps`, which are passed by the [router function](#nextadminrouter-function) via getServerSideProps -- `options` used to customize the UI, like field formatters for example. Do not use with App router. -- `dashboard` used to customize the rendered dashboard -- `translations` used to customize some of the texts displayed in the UI. See [i18n](/docs/i18n) for more details. -- `user` used to add some user information at the bottom of the menu. See [user properties](#user-properties) for more details. - -> ⚠️ : Do not override these `AdminComponentProps` props, they are used internally by Next Admin. - -This is an example of using the `NextAdmin` component with a custom Dashboard component and options: - -```tsx -// pages/admin/[[...nextadmin]].tsx -import Dashboard from "../../components/CustomDashboard"; - -export default function Admin(props: AdminComponentProps) { - /* Props are passed from the nextAdminRouter function via getServerSideProps */ - return ( - { - return {role.toString()}; - }, - }, - birthDate: { - formatter: (date) => { - return new Date(date as unknown as string) - ?.toLocaleString() - .split(" ")[0]; - }, - }, - }, - }, - }, - }, - }} - /> - ); -} -``` - -## Next Admin Options - -Next Admin options is the third parameter of the [router function](#nextadminrouter-function) and it's an object of options that has the following properties: - -### `basePath` - -`basePath` is a string that represents the base path of your admin. (e.g. `/admin`) - optional. - -### `model` - -`model` is an object that represents the customization options for each model in your schema. - -It takes as **_key_** a model name of your schema as **_value_** an object to customize your it. - -By default if no models are defined, they will all be displayed in the admin. If you want more control, you have to define each model individually as empty objects or with the following properties: - -| Name | Description | Default value | -| ------------- | --------------------------------------------------------------------------------------------------- | ---------------------------- | -| `toString` | a function that is used to display your record in related list | `id` field | -| `aliases` | an object containing the aliases of the model fields as keys, and the field name | undefined | -| `title` | a string used to display the model name in the sidebar and in the section title | Model name | -| `list` | an object containing the [list options](#list-property) | undefined | -| `edit` | an object containing the [edit options](#edit-property) | undefined | -| `actions` | an array of [actions](#actions-property) | undefined | -| `icon` | the [outline HeroIcon name](https://heroicons.com/outline) displayed in the sidebar and pages title | undefined | -| `permissions` | an array to specify restricted permissions on model | [`create`, `edit`, `delete`] | - -You can customize the following for each model: - -- ##### `list` property - -This property determines how your data is displayed in the [List View](/docs/glossary#list-view) - -| Name | Description | Default value | -| ----------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------- | -| `search` | an array of searchable fields | undefined - all scalar fields are searchable | -| `display` | an array of fields that are displayed in the list | undefined - all scalar fields are displayed | -| `fields` | an object containing the model fields as keys, and customization values, see [below](#fields-property) | undefined | -| `copy` | an array of fields that are copyable into the clipboard | undefined - no field is copyable by default | -| `defaultSort` | an optional object to determine the default sort to apply on the list | undefined | -| `defaultSort.field` | the model's field name on which the sort is applied. It is mandatory | | -| `defaultSort.direction` | the sort direction to apply. It is optional | | -| `filters` | define a set of Prisma filters that user can choose in list, see [below](#filters-property) | undefined | -| `exports` | an object or array of [export](#exports-property) - containing export url | undefined | - -> Note that the `search` property is only available for `scalar` fields. - -- ##### `edit` property - -This property determines how your data is displayed in the [edit view](/docs/glossary#edit-view) - -| Name | Description | Default value | -| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------- | -| `display` | an array of fields that are displayed in the form. It can also be an object that will be displayed in the form of a notice. See [notice](#notice) | all scalar fields are displayed | -| `styles` | an object containing the styles of the form | undefined | -| `fields` | an object containing the model fields as keys, and customization values | undefined | -| `submissionErrorMessage` | a message displayed if an error occurs during the form submission, after the form validation and before any call to prisma | Submission error | - -##### `styles` property - -The `styles` property is available in the `edit` property. - -> ⚠️ If your options are defined in a separate file, make sure to add the path to the `content` property of the `tailwind.config.js` file - -| Name | Description | -| ------- | ------------------------------------------------------------------------------ | -| `_form` | a string defining the classname of the form | -| ... | all fields of the model, with the field name as key and the classname as value | - -Here is an example of using `styles` property: - -```ts -styles: { - _form: "form-classname", - ... // all fields -}; -``` - -##### `fields` property - -The `fields` property is available in both `list` and `edit` properties. - -For the `list` property, it can take the following: - -| Name | Description | -| ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `formatter` | a function that takes the field value as a parameter, and that return a JSX node. It also accepts a second argument which is the [`NextAdmin` context](#nextadmin-context) | -| `sortBy` | available only on many-to-one models. The name of a field of the related model to apply the sort on. Defaults to the id field of the related model. | - -For the `edit` property, it can take the following: - -| Name | Description | -| ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `validate` | a function that takes the field value as a parameter, and that returns a boolean | -| `format` | a string defining an OpenAPI field format, overriding the one set in the generator. An extra `file` format can be used to be able to have a file input | -| `input` | a React Element that should receive [CustomInputProps](#custominputprops). For App Router, this element must be a client component. | -| `handler` | an object that can take the following properties | -| `handler.get` | a function that takes the field value as a parameter and returns a transformed value displayed in the form | -| `handler.upload` | an async function that is used only for formats `file` and `data-url`. It takes a Buffer object and and informations object containing `name` and `type` property as parameter and must return a string. Useful to upload a file to a remote provider | -| `handler.uploadErrorMessage` | an optional string displayed in the input field as an error message in case of a failure during the upload handler | -| `optionFormatter` | only for relation fields, a function that takes the field values as a parameter and returns a string. Useful to display your record in related list | -| `tooltip` | a tooltip content to show for the field | -| `helperText` | a helper text that is displayed underneath the input | -| `disabled` | a boolean to indicate that the field is read only | -| `display` | only for relation fields, indicate which display format to use between `list`, `table` or `select`. Default `select` | -| `required` | a true value to force a field to be required in the form, note that if the field is required by the Prisma schema, you cannot set `required` to false | -| `relationOptionFormatter` | same as `optionFormatter`, but used to format data that comes from an [explicit many-to-many](https://www.prisma.io/docs/orm/prisma-schema/data-model/relations/many-to-many-relations#explicit-many-to-many-relations) relationship. See [handling explicit many-to-many](/docs/code-snippets#explicit-many-to-many) | -| `orderField` | the field to use for relationship sorting. This allow to drag and drop the related records in the `list` display. | -| `relationshipSearchField` | a field name of the explicit many-to-many relation table to apply the search on. See [handling explicit many-to-many](/docs/code-snippets#explicit-many-to-many) | - -##### `filters` property - -The `filters` property allow you to define a set of Prisma filters that user can apply on list page. It's an array of the type below: - -| Name | Description | Default value | -| -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| `name` | an unique name for the filter | undefined | -| `active` | a boolean to set filter active by default | false | -| `value` | a where clause Prisma filter of the related model (e.g [Prisma operators](https://www.prisma.io/docs/orm/reference/prisma-client-reference#filter-conditions-and-operators)) | | - -##### `exports` property - -The `exports` property is available in the `list` property. It's an object or an array of objects that can take the following properties: - -| Name | Description | Default value | -| -------- | ----------------------------------------------------------------------------------- | ------------- | -| `format` | a string defining the format of the export. It's used to labeled the export button. | undefined | -| `url` | a string defining the URL of the export action. | undefined | - -> Note that the `exports` property do not take care of active filters. If you want to export filtered data, you have to add the filters in the URL or in your export action. - -### `pages` - -`pages` is an object that allows you to add your own sub pages as a sidebar menu entry. It is an object where the key is the path (without the base path) and the value is an object with the following properties: - -| Name | Description | -| ------- | ----------------------------------------------------------------------------------------------- | -| `title` | the title of the page displayed on the sidebar | -| `icon` | the [outline HeroIcon name](https://heroicons.com/outline) of the page displayed on the sidebar | - -#### `actions` property - -The `actions` property is an array of objects that allows you to define a set of actions that can be executed on one or more records of the resource. On the list view, there is a default action for deletion. The object can take the following properties: - -| Name | Description | Default value | -| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | ------------- | -| `title` | action title that will be shown in the action dropdown | undefined | -| `action` | an async function that will be triggered when selecting the action in the dropdown. For App Router, it must be defined as a server action. | undefined | -| `successMessage` | a message that will be displayed when the action is successful | undefined | -| `errorMessage` | a message that will be displayed when the action fails | undefined | - -#### `sidebar` property - -The `sidebar` property allows you to customise the aspect of the sidebar menu. It is an object that can take the following properties: - -| Name | Description | Default value | -| ----------------- | --------------------------------------------------------------------------------------------------- | ------------- | -| `groups` | an array of objects that creates groups for specific resources. It takes the following properties : | | -| `groups[].title` | the name of the group | | -| `groups[].models` | the model names to display in the group | | - -#### `externalLinks` property - -The `externalLinks` property allows you to add external links to the sidebar menu. It is an array of objects that can take the following properties: - -| Name | Description | -| ------- | ----------------------------------------------------------------- | -| `label` | the label of the link displayed on the sidebar. This is mandatory | -| `url` | the URL of the link. This is mandatory | - -### `defaultColorScheme` property - -The `defaultColorScheme` property defines a default color palette between `light`, `dark` and `system`, but allows the user to modify it. Default to `system`. - -### `forceColorScheme` property - -Identical to `defaultColorScheme` but does not allow the user to change it. - -Here is an example of using `NextAdminOptions` for the following schema : - -```prisma -// prisma/schema.prisma -enum Role { - USER - ADMIN -} - -model User { - id Int @id @default(autoincrement()) - email String @unique - name String? - password String @default("") - posts Post[] @relation("author") // One-to-many relation - profile Profile? @relation("profile") // One-to-one relation - birthDate DateTime? - createdAt DateTime @default(now()) - updatedAt DateTime @default(now()) @updatedAt - role Role @default(USER) -} -``` - -```tsx -// pages/api/admin/[[...nextadmin]].ts -const options: NextAdminOptions = { - basePath: "/admin", - model: { - User: { - toString: (user) => `${user.name} (${user.email})`, - list: { - display: ["id", "name", "email", "posts", "role", "birthDate"], - search: ["name", "email"], - fields: { - role: { - formatter: (role) => { - return {role.toString()}; - }, - }, - birthDate: { - formatter: (date) => { - return new Date(date as unknown as string) - ?.toLocaleString() - .split(" ")[0]; - }, - }, - }, - }, - edit: { - display: ["id", "name", "email", "posts", "role", "birthDate"], - fields: { - email: { - validate: (email) => email.includes("@") || "Invalid email", - }, - birthDate: { - format: "date", - }, - avatar: { - format: "file", - handler: { - upload: async (buffer, infos) => { - return "https://www.gravatar.com/avatar/00000000000000000000000000000000"; - }, - }, - }, - }, - }, - }, - }, -}; - -const adminRouter = await nextAdminRouter(prisma, schema, options); -``` - -## CustomInputProps - -This is the type of the props that are passed to the custom input component: - -| Name | Description | -| ----------- | --------------------------------------------------------------------------------------------------------------------------- | -| `name` | the field name | -| `value` | the field value | -| `onChange` | a function taking a [ChangeEvent](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/change_event) as a parameter | -| `readonly` | boolean indicating if the field is editable or not | -| `rawErrors` | array of strings containing the field errors | -| `disabled` | boolean indicating if the field is disabled | - -## NextAdmin Context - -The `NextAdmin` context is an object containing the following properties: - -- `locale`: the locale used by the calling page. (refers to the `accept-language` header). -- `row`: the current row of the list view. Represents non-formatted data of the current row. - -## Notice - -The edit page's form can display notice alerts. To do so, you can pass objects in the `display` array of the `edit` property of a model. This object takes the following properties : - -| Name | Description | -| ------------- | ------------------------------------------------------------------------------------------------------------- | -| `title` | The title of the notice. This is mandatory | -| `id` | A unique identifier for the notice that can be used to style it with the `styles` property. This is mandatory | -| `description` | The description of the notice. This is optional | - -## User properties - -The `user` property is an object that can take the following properties: - -| Name | Description | -| -------------- | -------------------------------------------------------------------- | -| `data.name` | the name of the user displayed in the sidebar menu. This is required | -| `data.picture` | the URL of the user's avatar displayed in the sidebar menu | -| `logoutUrl` | an URL or path to logout the user. This is required. | diff --git a/apps/docs/pages/docs/api/_meta.json b/apps/docs/pages/docs/api/_meta.json new file mode 100644 index 00000000..01cf6334 --- /dev/null +++ b/apps/docs/pages/docs/api/_meta.json @@ -0,0 +1,20 @@ +{ + "next-admin-component": { + "title": "" + }, + "options": { + "title": "Options parameter" + }, + "model-configuration": { + "title": "Model configuration" + }, + "user": { + "title": "User" + }, + "create-handler-function": { + "title": "createHandler()" + }, + "get-next-admin-props-function": { + "title": "getNextAdminProps()" + } +} diff --git a/apps/docs/pages/docs/api/create-handler-function.mdx b/apps/docs/pages/docs/api/create-handler-function.mdx new file mode 100644 index 00000000..894dd347 --- /dev/null +++ b/apps/docs/pages/docs/api/create-handler-function.mdx @@ -0,0 +1,32 @@ + +import { Callout, Tabs } from "nextra/components"; +import OptionsTable from "../../../components/OptionsTable"; + +# `createHandler` function + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs) + + +`createHandler` is a function that returns an object that allows you to catch all API access. It accepts an object with the following properties: + +Your Next Admin options - optional. + }, + + ]} +/> diff --git a/apps/docs/pages/docs/api/get-next-admin-props-function.mdx b/apps/docs/pages/docs/api/get-next-admin-props-function.mdx new file mode 100644 index 00000000..5d5a5ad2 --- /dev/null +++ b/apps/docs/pages/docs/api/get-next-admin-props-function.mdx @@ -0,0 +1,101 @@ + +import { Callout, Tabs } from "nextra/components"; +import OptionsTable from "../../../components/OptionsTable"; + +# `getNextAdminProps` function + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs) + + + + + The following is used only for App router. + + `getNextAdminProps` is a function that returns the props for the [`NextAdmin`](/docs/api/next-admin-component) component. It accepts one argument which is an object with the following properties: + + The array of route params retrieved from the optional catch-all segment + }, + { + name: 'searchParams', + description: <> The query params retrieved from the page + }, + { + name: 'basePath', + description: 'The base path of your admin. It is used to build the admin URL. It is mandatory.' + }, + { + name: 'apiBasePath', + description: 'The base path of your admin API. It is used to build the admin API URL. It is mandatory.' + }, + { + name: 'schema', + description: <>The JSON schema generated by the prisma generate command. + }, + { + name: 'prisma', + description: 'Your Prisma client instance.' + }, + { + name: 'getMessages', + description: <>A function with no parameters that returns translation messages. It is used to translate the default messages of the library. See i18n for more details. + }, + { + name: 'locale', + description: <>The locale used, find from params (e.g. en, fr). + }, + { + name: 'options', + description: <>The options object - optional. + } + ]} + /> + + + + The following is used only for Pages router + + `getNextAdminProps` is a function that returns the props for the [`NextAdmin`](/docs/api/next-admin-component) component. It accepts one argument which is an object with the following properties: + + The base path of your admin. It is used to build the admin URL. It is optional and defaults to /admin. + }, + { + name: 'apiBasePath', + description: <>The base path of your admin API. It is used to build the admin API URL. It is optional and defaults to /api/admin. + }, + { + name: 'schema', + description: <>The JSON schema generated by the prisma generate command. + }, + { + name: 'prisma', + description: 'Your Prisma client instance.' + }, + { + name: 'req', + description: <>The request object from the page (IncomingMessage). + }, + { + name: 'getMessages', + description: <>A function with no parameters that returns translation messages. It is used to translate the default messages of the library. See i18n for more details. + }, + { + name: 'locale', + description: <>The locale used, find from params (e.g. en, fr). + }, + { + name: 'options', + description: <>The options object - optional. + } + + ]} + /> + + diff --git a/apps/docs/pages/docs/api/model-configuration.mdx b/apps/docs/pages/docs/api/model-configuration.mdx new file mode 100644 index 00000000..759ab64f --- /dev/null +++ b/apps/docs/pages/docs/api/model-configuration.mdx @@ -0,0 +1,554 @@ +import { Callout, Tabs } from "nextra/components"; +import OptionsTable from "../../../components/OptionsTable"; + +# Model configuration + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs) + + +To configure your models, you can use the `model` property of the [NextAdmin options](/docs/api/options) parameter. + +This property allows you to configure everything about how your models, their fields, their relations are displayed and edited. + +Example for a schema with a `User`, `Post` and `Category` model: + + +```tsx copy +import { NextAdminOptions } from "@premieroctet/next-admin"; + +export const options: NextAdminOptions = { + /* Your other options here */ + model: { + User: { + toString: (user) => `${user.name} (${user.email})`, + title: "Users", + icon: "UsersIcon", + list: { + display: ["id", "name", "email", "posts", "role", "birthDate"], + search: ["name", "email"], + filters: [ + { + name: "is Admin", + active: false, + value: { + role: { + equals: "ADMIN", + }, + }, + }, + ], + }, + edit: { + display: [ + "id", + "name", + "email", + "posts", + "role", + "birthDate", + "avatar", + ], + }, + }, + Post: { + toString: (post) => `${post.title}`, + }, + Category: { + title: "Categories", + icon: "InboxStackIcon", + toString: (category) => `${category.name}`, + list: { + display: ["name", "posts"], + }, + edit: { + display: ["name", "posts"], + }, + }, + }, +}; + +``` + +It takes as **key** a model name of your schema as **value** an object to configure it. + +By default, if no models are defined, they will all be displayed in the admin. If you want more control, you have to define each model individually as empty objects or with the following properties: + +an object containing the list options (see list property) + }, + { + name: "edit", + description: <>an object containing the edit options (see edit property) + }, + { + name: "actions", + description: <> an array of actions (see actions property) + }, + { + name: "icon", + description: <>the outline HeroIcon name displayed in the sidebar and pages title, + }, + { + name: "permissions", + description: "an array to specify restricted permissions on model", + defaultValue: "[`create`, `edit`, `delete`]" + }, + ]} /> + +You can customize the following for each model: + +## `list` property + +This property determines how your data is displayed in the [list View](/docs/glossary#list-view) +an object containing the model fields as keys, and customization values (see fields property), + }, + { + name: "copy", + type: "Array", + description: "an array of fields that are copyable into the clipboard", + defaultValue: "undefined - no field is copyable by default", + }, + { + name: "defaultSort", + type: "Object", + description: "an optional object to determine the default sort to apply on the list", + }, + { + name: "defaultSort.field", + type: "String", + description: "the model's field name to which the sort is applied. It is mandatory", + }, + { + name: "defaultSort.direction", + type: "String", + description: "the sort direction to apply. It is optional", + }, + { + name: "filters", + type: "Array", + description: <> define a set of Prisma filters that user can choose in list (see filters), + }, + { + name: "exports", + type: "Object", + description: <>an object or array of export - containing export url (see exports), + }, + ]} +/> + + The `search` property is only available for [`scalar` fields](https://www.prisma.io/docs/orm/reference/prisma-schema-reference#model-field-scalar-types). + + +#### `list.fields` property +The `fields` property is an object that can have the following properties: + +a function that takes the field value as a parameter, and returns a JSX node. It also accepts a second argument which is the NextAdmin context, + }, + { + name: "sortBy", + type: "String", + description: "available only on many-to-one models. The name of a field in the related model to apply the sort to. Defaults to the id field of the related model", + }, + ]} +/> + +##### `list.filters` property + +The `filters` property allow you to define a set of Prisma filters that user can apply on list page. It's an array of the type below: + +a where clause Prisma filter of the related model (e.g. Prisma operators), + }, + ]} +/> + +#### `list.exports` property + +The `exports` property is available in the `list` property. It's an object or an array of objects that can have the following properties: + + + + + The `exports` property does not account for active filters. If you want to export filtered data, you need to add the filters to the URL or in your export action. + + + + +## `edit` property + +This property determines how your data is displayed in the [edit view](/docs/glossary#edit-view) + an array of fields that are displayed in the form. It can also be an object that will be displayed in the form of a notice (see notice), + defaultValue: "all scalar fields are displayed", + }, + { + name: "styles", + type: "Object", + description: <>an object containing the styles of the form (see styles) , + }, + { + name: "fields", + type: "Object", + description: <>an object containing the model fields as keys, and customization values (see fields property), + }, + { + name: "submissionErrorMessage", + type: "String", + description: "a message displayed if an error occurs during the form submission", + defaultValue: "'Submission error'", + }, + ]} + /> + +#### `edit.styles` property + +The `styles` property is available in the `edit` property. + + If your options are defined in a separate file, make sure to add the path to the `content` property of the `tailwind.config.js` file (see [TailwindCSS configuration](/docs/getting-started#tailwindcss-configuration)). + + + + +Here is an example of using `styles` property: + +```ts +styles: { + _form: "form-classname", + ... // all fields +}; +``` + +#### `edit.fields` property + +This property can be defined for each field of the corresponding model. +When you define a field, use the field's name as the key and the following object as the value: + +a string defining an OpenAPI field format, overriding the one set in the generator. An extra file format can be used to be able to have a file input, + }, + { + name: "input", + type: "React Element", + description: <>a React Element that should receive CustomInputProps. For App Router, this element must be a client component, + }, + { + name: "handler", + type: "Object", + description: "", + }, + { + name: "handler.get", + type: "Function", + description: "a function that takes the field value as a parameter and returns a transformed value displayed in the form", + }, + { + name: "handler.upload", + type: "Function", + description: <>an async function that is used only for formats file and data-url. It takes a Buffer object and an information object containing name and type properties as parameters and must return a string. It can be useful to upload a file to a remote provider, + }, + { + name: "handler.uploadErrorMessage", + type: "String", + description: "an optional string displayed in the input field as an error message in case of a failure during the upload handler", + }, + { + name: "optionFormatter", + type: "Function", + description: "only for relation fields, a function that takes the field values as a parameter and returns a string. Useful to display your record in related list", + }, + { + name: "tooltip", + type: "String", + description: "A tooltip content to display for the field ", + }, + { + name: "helperText", + type: "String", + description: "a helper text that is displayed underneath the input", + }, + { + name: "disabled", + type: "Boolean", + description: "a boolean to indicate that the field is read only", + }, + { + name: "display", + type: "String", + description: <> only for relation fields, indicate which display format to use between list, table or select, + defaultValue: "select", + }, + { + name: "required", + type: "Boolean", + description: <>a true value to force a field to be required in the form, note that if the field is required by the Prisma schema, you cannot set required to false, + }, + { + name: "relationOptionFormatter", + type: "Function", + description: <>same as optionFormatter, but used to format data that comes from an explicit many-to-many relationship, + }, + { + name: "orderField", + type: "String", + description: <>the field to use for relationship sorting. This allows to drag and drop the related records in the list display, + }, + { + name: "relationshipSearchField", + type: "String", + description: <>a field name of the explicit many-to-many relation table to apply the search on. See handling explicit many-to-many, + }, + ]} + /> + + + +## `actions` property + +The `actions` property is an array of objects that allows you to define a set of actions that can be executed on one or more records of the resource. On the list view, there is a default action for deletion. The object can have the following properties: + +an async function that will be triggered when selecting the action in the dropdown. For App Router, it must be defined as a server action, + }, + { + name: "successMessage", + type: "String", + description: "a message that will be displayed when the action is successful", + }, + { + name: "errorMessage", + type: "String", + description: "a message that will be displayed when the action fails", + }, + ]} +/> + +## NextAdmin Context + +The `NextAdmin` context is an object containing the following properties: + +The locale used by the calling page. (refers to the accept-language header), + }, + { + name: "row", + type: "Object", + description: "The current row of the list view", + }, + ]} +/> + + +## Notice + +The edit page's form can display notice alerts. To do so, you can pass objects in the `display` array of the `edit` property of a model. +This can be useful to display alerts or information to the user when editing a record. +This object takes the following properties : + +A unique identifier for the notice that can be used to style it with the styles property. This is mandatory, + }, + { + name: "description", + type: "String", + description: "The description of the notice. This is optional", + }, + ]} +/> + +Here is a quick example of usage. + +Considering you have a model `User` with the following configuration: + +```typescript filename=".../options.ts" copy {13-17} +export const options: NextAdminOptions = { + basePath: "/admin", + title: "⚡️ My Admin", + model: { + User: { + /** + ...some configuration + **/ + edit: { + display: [ + "id", + "name", + { + title: "Email is mandatory", + id: "email-notice", + description: "You must add an email from now on", + } as const, + "email", + "posts", + "role", + "birthDate", + "avatar", + "metadata", + ], + /** ... some configuration */ + }, + }, + }, +}; +``` + +In this example, the `email-notice` notice will be displayed in the form before the `email` field. +![Notice example](/docs/notice-exemple.png) + +## CustomInputProps +Represents the props that are passed to the custom input component. + a function taking a ChangeEvent as a parameter, + }, + { + name: "readonly", + type: "Boolean", + description: "boolean indicating if the field is editable or not", + }, + { + name: "rawErrors", + type: "Array", + description: "array of strings containing the field errors", + }, + { + name: "disabled", + type: "Boolean", + description: "boolean indicating if the field is disabled", + }, + ]} +/> \ No newline at end of file diff --git a/apps/docs/pages/docs/api/next-admin-component.mdx b/apps/docs/pages/docs/api/next-admin-component.mdx new file mode 100644 index 00000000..d6da1adc --- /dev/null +++ b/apps/docs/pages/docs/api/next-admin-component.mdx @@ -0,0 +1,34 @@ +import { Callout, Tabs } from "nextra/components"; + +# `` component + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs) + + +`` is a React component that contains the entire UI of Next Admin. It can take several props from `getNextAdminProps` function, but also some additional props: + +- `options` used to customize the UI, like field formatters for example. Do not use with App router (see [Options](/docs/api/options) for more details). +- `dashboard` used to customize the rendered dashboard. +- `user` used to add some user information at the bottom of the menu. See [user properties](/docs/api/options) for more details. + +This is an example using the `NextAdmin` component with a custom Dashboard component and options: + +```tsx +import Dashboard from "path/to/CustomDashboard"; + +export default function Admin(props: AdminComponentProps) { + /* Props are passed from getNextAdminProps function */ + return ( + + ); +} +``` \ No newline at end of file diff --git a/apps/docs/pages/docs/api/options.mdx b/apps/docs/pages/docs/api/options.mdx new file mode 100644 index 00000000..685da306 --- /dev/null +++ b/apps/docs/pages/docs/api/options.mdx @@ -0,0 +1,201 @@ +import { Callout, Tabs } from "nextra/components"; +import OptionsTable from "../../../components/OptionsTable"; + +# Next Admin options parameter + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs) + + +Next Admin `options` is a parameter that allows you to configure your admin panel to your needs. + +Example: + +```tsx copy +import { NextAdminOptions } from "@premieroctet/next-admin"; + +export const options: NextAdminOptions = { + title: "⚡️ My Admin Page", + model: { + /* Your model configuration here */ + }, + pages: { + "/custom": { + title: "Custom page", + icon: "AdjustmentsHorizontalIcon", + }, + }, + externalLinks: [ + { + label: "App Router", + url: "/", + }, + ], + sidebar: { + groups: [ + { + title: "Users", + models: ["User"], + }, + { + title: "Categories", + models: ["Category"], + }, + ], + }, +}; +``` + + + It is recommended to centralize your options in a single file, as it might be used in multiple places. + + + +## `title` + +The `title` property is a string that represents the title of the admin dashboard. It is displayed in the sidebar. By default, it is set to "Admin". + +## `model` + +`model` is a property that allows you to configure everything about how your models, their fields, their relations are displayed and edited. It's highly configurable you can learn more about it [here](/docs/api/model-configuration). + +## `pages` + +`pages` is an object that allows you to add your own sub-pages as a sidebar menu entry. It is an object where the key is the path (without the base path) and the value is an object with the following properties: + +The outline HeroIcon name of the page displayed on the sidebar, + }, + ]} +/> + +## `sidebar` + +The `sidebar` property allows you to customize the aspect of the sidebar menu. It is an object that can have the following properties: + + + +## `externalLinks` + +The `externalLinks` property allows you to add external links to the sidebar menu. It is an array of objects that can have the following properties: + + + +## `defaultColorScheme` + +The `defaultColorScheme` property defines a default color palette between `light`, `dark` and `system`, but allows the user to modify it. Default to `system`. + +## `forceColorScheme` + +Identical to `defaultColorScheme` but does not allow the user to change it. + +Here is an example of using `NextAdminOptions` for the following schema : + +```prisma +// prisma/schema.prisma +enum Role { + USER + ADMIN +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + password String @default("") + posts Post[] @relation("author") // One-to-many relation + profile Profile? @relation("profile") // One-to-one relation + birthDate DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + role Role @default(USER) +} +``` + +```tsx +// pages/api/admin/options.ts +export const options: NextAdminOptions = { + basePath: "/admin", + model: { + User: { + toString: (user) => `${user.name} (${user.email})`, + list: { + display: ["id", "name", "email", "posts", "role", "birthDate"], + search: ["name", "email"], + fields: { + role: { + formatter: (role) => { + return {role.toString()}; + }, + }, + birthDate: { + formatter: (date) => { + return new Date(date as unknown as string) + ?.toLocaleString() + .split(" ")[0]; + }, + }, + }, + }, + edit: { + display: ["id", "name", "email", "posts", "role", "birthDate"], + fields: { + email: { + validate: (email) => email.includes("@") || "Invalid email", + }, + birthDate: { + format: "date", + }, + avatar: { + format: "file", + handler: { + upload: async (buffer, infos) => { + return "https://www.gravatar.com/avatar/00000000000000000000000000000000"; + }, + }, + }, + }, + }, + }, + }, +}; +``` \ No newline at end of file diff --git a/apps/docs/pages/docs/api/user.mdx b/apps/docs/pages/docs/api/user.mdx new file mode 100644 index 00000000..29dc2ed4 --- /dev/null +++ b/apps/docs/pages/docs/api/user.mdx @@ -0,0 +1,30 @@ +import { Callout, Tabs } from "nextra/components"; +import OptionsTable from "../../../components/OptionsTable"; + +# User + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs) + + +The `user` property allows you to define the user displayed in the sidebar menu. +Its an object that can have the following properties: + +either a request tuple ([RequestInfo, RequestInit?]) to fetch the logout API, a function to call on logout (server action), or a string to redirect to a logout page, + }, + ]} +/> \ No newline at end of file diff --git a/apps/docs/pages/docs/code-snippets.mdx b/apps/docs/pages/docs/code-snippets.mdx index 90182a13..9539d242 100644 --- a/apps/docs/pages/docs/code-snippets.mdx +++ b/apps/docs/pages/docs/code-snippets.mdx @@ -1,17 +1,138 @@ +import { Callout, Tabs } from "nextra/components"; + # Code snippets + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs/code-snippets) + + This page contains code snippets that you can use in your projects. These are not a part of Next Admin, but may be useful for your projects. Some of the snippets are implemented in the example project. You can check them out in the [example project](https://next-admin-po.vercel.app) or in the [source code](https://github.com/premieroctet/next-admin/tree/main/apps/example). +## Authentication + + + + The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check in the page: + + > The following example uses [next-auth](https://next-auth.js.org/) to handle authentication + + ```ts copy filename="app/api/admin/[[...nextadmin]]/route.ts" + const { run } = createHandler({ + options, + prisma, + apiBasePath: "/api/admin", + schema, + onRequest: (req) => { + const session = await getServerSession(authOptions); + const isAdmin = session?.user?.role === "SUPERADMIN"; + + if (!isAdmin) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + } + }); + + export { run as POST, run as GET, run as DELETE }; + ``` + + ```tsx copy filename="pages/admin/[[...nextadmin]].tsx" + export default async function AdminPage({ + params, + searchParams, + }: { + params: { [key: string]: string[] }; + searchParams: { [key: string]: string | string[] | undefined } | undefined; + }) { + const session = await getServerSession(authOptions); + const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check + + if (!isAdmin) { + redirect('/', { permanent: false }) + } + + const props = await getNextAdminProps({ + params: params.nextadmin, + searchParams, + basePath: "/admin", + apiBasePath: "/api/admin", + prisma, + schema, + }); + + return ; + } + ``` + + + + The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check to the `getServerSideProps` function: + + > The following example uses [next-auth](https://next-auth.js.org/) to handle authentication + + ```ts copy + export const config = { + api: { + bodyParser: false, + }, + }; + + const { run } = createHandler({ + prisma, + options, + apiBasePath: "/api/admin", + schema: schema, + onRequest: (req, res, next) => { + const session = await getServerSession(req, res, authOptions); + const isAdmin = session?.user?.role === "SUPERADMIN"; + + if (!isAdmin) { + return res.status(403).json({ error: 'Forbidden' }) + } + + return next() + } + }); + + export default run; + ``` + + ```ts copy filename="pages/api/admin/[[...nextadmin]].ts" + export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const session = await getServerSession(req, res, authOptions); + const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check + + if (!isAdmin) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + return await getNextAdminProps({ + basePath: "/pagerouter/admin", + apiBasePath: "/api/pagerouter/admin", + prisma, + schema, + options, + req, + }); + }; + ``` + + + + ## Export data -By using [exports](/docs/api-docs#exports-property) options, you can export data. Next Admin only implements the CSV export button, it's actually a link pointing to a provided url. +By using [exports](/docs/api/model-configuration#listexports-property) options, you can export data. Next Admin only implements the CSV export button, it's actually a link pointing to a provided url. The API endpoint route must be defined in the `exports` property of the options object. This is an example of how to implement the export API endpoint: -```typescript copy -// app/api/users/export/route.ts +```typescript copy filename="app/api/admin/[[...nextadmin]]/route.ts" import { prisma } from "@/prisma"; export async function GET() { @@ -32,8 +153,7 @@ export async function GET() { or with a stream response: -```typescript copy -// app/api/users/export/route.ts +```typescript copy filename="app/api/admin/[[...nextadmin]]/route.ts" import { prisma } from "@/prisma"; const BATCH_SIZE = 1000; @@ -79,7 +199,7 @@ export async function GET() { > Note that you must secure the export route if you don't want to expose your data to the public by adding authentication middleware. -There is two example files in the example project: +There are two example files in the example project: - [app/api/users/export/route.ts](https://github.com/premieroctet/next-admin/tree/main/apps/example/app/api/users/export/route.ts) - [app/api/posts/export/route.ts](https://github.com/premieroctet/next-admin/tree/main/apps/example/app/api/posts/export/route.ts) @@ -88,25 +208,41 @@ There is two example files in the example project: If you want to add data to the form data before submitting it, you can add logic to the `submitFormAction` function. This is an example of how to add `createdBy` and `updatedBy` fields based on the user id: -```typescript copy -// actions/nextadmin.ts -"use server"; -import { ActionParams } from "@premieroctet/next-admin"; -import { submitForm } from "@premieroctet/next-admin/dist/actions"; - -export const submitFormAction = async ( - params: ActionParams, - formData: FormData -) => { - const userId = /* get the user id */; - if (params.params[1] === "new") { - formData.append("createdBy", userId); +```typescript copy filename="app/api/admin/[[...nextadmin]]/route.ts" +import { options } from "@/options"; +import { prisma } from "@/prisma"; +import schema from "@/prisma/json-schema/json-schema.json"; +import { createHandler } from "@premieroctet/next-admin/dist/appHandler"; +import { NextRequest } from "next/server"; + +const { run } = createHandler({ + apiBasePath: "/api/admin", + options, + prisma, + schema, +}); + +/** + * context.params.nextadmin = ['{{model}}'] => CREATE + * context.params.nextadmin = ['{{model}}', '{{id}}'] => UPDATE + */ +export async function POST(req: NextRequest, context: any) { + const userId = 1; + + if (context.params.nextadmin.length === 1) { + const formData = await req.formData(); + formData.append("createdBy", userId.toString()); + req.formData = async () => formData; } else { - formData.append("updatedBy", userId); + const formData = await req.formData(); + formData.append("updatedBy", userId.toString()); + req.formData = async () => formData; } - return submitForm({ ...params, options, prisma }, formData); -}; + return run(req, context); +} + +export { run as DELETE, run as GET }; ``` > Note that this example assumes that you have a `createdBy` and `updatedBy` field on each model, if you need to check the model name, you can use `params.params[0]`. @@ -117,8 +253,7 @@ This snippet is not implemented in the example project. If you want to customize the input form, you can create a custom input component. This is an example of how to create a custom input component for the `birthDate` field: -```typescript copy -// components/inputs/BirthDateInput.tsx +```typescript copy filename="app/components/inputs/BirthDateInput.tsx" "use client"; import { CustomInputProps } from "@premieroctet/next-admin"; import DateTimePicker from "react-datepicker"; @@ -226,3 +361,46 @@ Note that you will need to use `relationOptionFormatter` instead of `optionForma With the `list` display, if the `orderField` property is defined, you will be able to apply drag and drop sorting on the categories. Upon form submission, the order will be updated accordingly, starting from 0. > The `orderField` property can also be applied for one-to-many relationships. In that case, drag and drop will also be available. + +# Custom pages + +You can create custom pages in the Next Admin UI. By reusing the `MainLayout` component, you can create a new page with the same layout as the Next Admin pages. This is an example of how to create a custom page: + +```typescript copy filename="app/custom/page.tsx" + +import { MainLayout } from "@premieroctet/next-admin"; +import { getMainLayoutProps } from "@premieroctet/next-admin/dist/appRouter"; +import { options } from "@/options"; +import { prisma } from "@/prisma"; + +const CustomPage = async () => { + const mainLayoutProps = getMainLayoutProps({ + basePath: "/admin", + apiBasePath: "/api/admin", + /*options*/ + }); + + return ( + + {/*Page content*/} + + ); +}; + +export default CustomPage; +``` + +Then, if you want that route to be available on the sidebar, you can add a new route - [more info](/docs/api/options#pages): + +```typescript copy +{ + [...] + pages: { + "/custom": { + title: "Custom page", + icon: "PresentationChartBarIcon", + }, + }, + [...] +} +``` diff --git a/apps/docs/pages/docs/edge-cases.mdx b/apps/docs/pages/docs/edge-cases.mdx index 79f79798..11f7dc96 100644 --- a/apps/docs/pages/docs/edge-cases.mdx +++ b/apps/docs/pages/docs/edge-cases.mdx @@ -1,5 +1,12 @@ +import { Callout } from "nextra/components"; + # Edge cases + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs/edge-cases) + + + In this part, we will talk about the edge cases we found during the development of the project. Prisma allows different structures to define models and relations. We had to choose which to exploit and which to avoid. Some choices may evolve in the future. @@ -20,16 +27,15 @@ model User { ### Autogenerated fields -Prisma allows fields to be generated automatically. For example, the `id` field is automatically generated by Prisma. These fields are not editable by the user, neither during creation nor during update. - +Prisma allows fields to be generated automatically. For example, the `id` field is automatically generated by Prisma. These fields are not editable by the user, neither during creation nor during an update. ## Relations ### One to one -Prisma allows one-to-one relations. But just one of the two models can have a relation field. If you want to remove the relation, you have to remove the field from the model that don't have the relation field. +Prisma allows one-to-one relations. But just one of the two models can have a relation field. If you want to remove the relation, you have to remove the field from the model that doesn't have the relation field. -There is an example of one-to-one relation between `User` and `Profile` models. +There is an example of a one-to-one relation between `User` and `Profile` models. ```prisma model User { @@ -49,8 +55,8 @@ Even if the `userId` field is not required, you cannot remove the relationship b ### Many to many -Prisma allows two types of [many-to-many](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations) relationships: implicit and explicit. Both are supported in the library. But for better understanding in the Next Admin UI, we recommend to use implicit relations. +Prisma allows two types of [many-to-many](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations) relationships: implicit and explicit. Both are supported in the library. But for better understanding in the Next Admin UI, we recommend using implicit relations. ### Self relation -Prisma allows [self relations](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations). All the self relations are supported by the library. \ No newline at end of file +Prisma allows [self relations](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations). All the self relations are supported by the library. diff --git a/apps/docs/pages/docs/getting-started.mdx b/apps/docs/pages/docs/getting-started.mdx index 39e413f0..c32ae789 100644 --- a/apps/docs/pages/docs/getting-started.mdx +++ b/apps/docs/pages/docs/getting-started.mdx @@ -1,33 +1,40 @@ -import { Callout, Tabs } from "nextra/components"; +import { Callout, Steps, Tabs } from "nextra/components"; # Getting Started -## Installation + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs/getting-started) + + +The following guide will help you get started with Next-Admin. + + +### Installation - ```bash + ```bash copy yarn add @premieroctet/next-admin prisma-json-schema-generator ``` - ```bash + ```bash copy npm install -S @premieroctet/next-admin prisma-json-schema-generator ``` - ```bash + ```bash copy pnpm install -S @premieroctet/next-admin prisma-json-schema-generator ``` -## TailwindCSS configuration +### TailwindCSS -Next-Admin relies on [TailwindCSS](https://tailwindcss.com/) for the style. If you do not have it, you can [install TailwindCSS](https://tailwindcss.com/docs/installation) with the following config : +Next-Admin relies on [TailwindCSS](https://tailwindcss.com/) for the style. If you do not have it, you can [install TailwindCSS](https://tailwindcss.com/docs/installation) with the following configuration : -```typescript +```typescript copy filename="tailwind.config.js" module.exports = { content: [ "./node_modules/@premieroctet/next-admin/dist/**/*.{js,ts,jsx,tsx}", @@ -41,27 +48,130 @@ Then import your `.css` file containing Tailwind rules into a page file or a par You can find more information about theming [here](/docs/theming) -## SuperJson configuration +### Prisma + +Next-Admin relies on [Prisma](https://www.prisma.io/) for the database. If you do not have it, you can [install Prisma](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-typescript-postgres) with the following config. + +You have to add the `jsonSchema` generator to your `schema.prisma` file: + +```prisma copy filename="schema.prisma" +generator jsonSchema { + provider = "prisma-json-schema-generator" + includeRequiredFields = "true" +} +``` + +Then run the following command : + +```bash copy +yarn run prisma generate +``` + +### Page `[[...nextadmin]]` + +Next-Admin uses a dynamic route `[[...nextadmin]]` to handle all the admin routes. + + + + + #### Create file : `page.tsx` + + ```tsx copy filename="app/admin/[[...nextadmin]]/page.tsx" + import { NextAdmin, PageProps } from "@premieroctet/next-admin"; + import { getNextAdminProps } from "@premieroctet/next-admin/dist/appRouter"; + import { prisma } from "@/prisma"; + import schema from "@/prisma/json-schema/json-schema.json"; + import "@/styles.css" // .css file containing tailiwnd rules + + export default async function AdminPage({ + params, + searchParams, + }: PageProps) { + const props = await getNextAdminProps({ + params: params.nextadmin, + searchParams, + basePath: "/admin", + apiBasePath: "/api/admin", + prisma, + schema, + /*options*/ + }); + + return ; + } + ``` + + + + Passing the `options` prop like you'd do on Pages router will result in an error in case you + have functions defined inside the options object (formatter, handlers, etc.). + Make sure to pass no `options` prop to `NextAdmin` component in App router. + + + + + Make sure to not use `use client` in the page. + + + + + + #### Create file : `[[...nextadmin]].ts` + + ```tsx copy filename="pages/admin/[[...nextadmin]].ts" + import { AdminComponentProps, NextAdmin } from "@premieroctet/next-admin"; + + import { getNextAdminProps } from "@premieroctet/next-admin/dist/pageRouter"; + import { GetServerSideProps } from "next"; + import { prisma } from " @/prisma"; + import schema from "@/prisma/json-schema/json-schema.json"; + import "@/styles.css"; + + export default function Admin(props: AdminComponentProps) { + return ( + + ); + } + + export const getServerSideProps: GetServerSideProps = async ({ req }) => + await getNextAdminProps({ + basePath: "/pagerouter/admin", + apiBasePath: "/api/pagerouter/admin", + prisma, + schema, + /*options*/ + req, + }); + ``` + + + Do not forget to add the `options` prop to `` component and `getNextAdminProps` function, if you are using it. + + + #### SuperJson configuration SuperJson is required to avoid errors related to invalid serialisation properties that can occur when passing data from server to client. -### With Babel +##### With Babel ```bash -yarn add -D babel-plugin-superjson-next superjson@^1 + yarn add -D babel-plugin-superjson-next superjson@^1 ``` ```bash -npm install --save-dev babel-plugin-superjson-next superjson@^1 + npm install --save-dev babel-plugin-superjson-next superjson@^1 ``` ```bash -pnpm install -D babel-plugin-superjson-next superjson@^1 + pnpm install -D babel-plugin-superjson-next superjson@^1 ``` @@ -75,7 +185,7 @@ Add the `babel-plugin-superjson-next` plugin to your `.babelrc` file: } ``` -## With SWC (Experimental) +##### With SWC (Experimental) @@ -114,124 +224,78 @@ module.exports = { }; ``` -## Quick Start + -Add the `prisma-json-schema-generator` generator to your `schema.prisma` file: + More information about the `getNextAdminProps` [here](/docs/api/get-next-admin-props-function). -```prisma -generator jsonSchema { - provider = "prisma-json-schema-generator" - includeRequiredFields = "true" -} -``` - -Then run the following command : + -```bash -yarn run prisma generate -``` +### API Route `[[...nextadmin]]` -This will create a `json-schema.json` file in the `prisma/json-schema` directory. +Next-Admin uses a dynamic route `[[...nextadmin]]` to handle all the API routes. - + - ```tsx - // app/admin/[[...nextadmin]]/page.tsx - import { NextAdmin } from "@premieroctet/next-admin"; - import { getPropsFromParams } from "@premieroctet/next-admin/dist/appRouter"; - import Dashboard from "../../../components/Dashboard"; - import { options } from "../../../options"; - import { prisma } from "../../../prisma"; - import schema from "../../../prisma/json-schema/json-schema.json"; - import { submitFormAction } from "../../../actions/nextadmin"; - import "../../../styles.css" // .css file containing tailiwnd rules - export default async function AdminPage({ - params, - searchParams, - }: { - params: { [key: string]: string[] }; - searchParams: { [key: string]: string | string[] | undefined } | undefined; - }) { - const props = await getPropsFromParams({ - params: params.nextadmin, - searchParams, - options, - prisma, - schema, - action: submitFormAction, - }); + ```ts copy filename="app/api/admin/[[...nextadmin]]/route.ts" + import { prisma } from "@/prisma"; + import schema from "@/prisma/json-schema/json-schema.json"; + import { createHandler } from "@premieroctet/next-admin/dist/appHandler"; - return ; - } - ``` + const { run } = createHandler({ + apiBasePath: "/api/admin", + prisma, + schema, + /*options*/ + }); - - - Passing the `options` prop like you'd do on Page router will result in an error in case you - have functions defined inside the options object (formatter, handlers, etc.). - Make sure to pass no `options` prop at all. + export { run as DELETE, run as GET, run as POST }; + ``` - + + - + ```ts copy filename="pages/api/admin/[[...nextadmin]].ts" + import { prisma } from "@/prisma"; + import { createHandler } from "@premieroctet/next-admin/dist/pageHandler"; + import schema from "@/prisma/json-schema/json-schema.json"; - Make sure to not use `"use client"` in the page. + export const config = { + api: { + bodyParser: false, + }, + }; - + const { run } = createHandler({ + apiBasePath: "/api/admin", + prisma, + schema: schema, + /*options*/, + }); - You will also need to create the action: - - ```tsx - // actions/nextadmin.ts - "use server"; - import { ActionParams } from "@premieroctet/next-admin"; - import { submitForm } from "@premieroctet/next-admin/dist/actions"; - import { prisma } from "../prisma"; - import { options } from "../options"; - - export const submitFormAction = async ( - params: ActionParams, - formData: FormData - ) => { - return submitForm({ ...params, options, prisma }, formData); - }; - ``` + export default run; + ``` + + Make sure to export the config object to define no `bodyParser`. This is required to be able to parse FormData. + - - ```tsx - // pages/admin/[[...nextadmin]].tsx - import { GetServerSideProps, GetServerSidePropsResult } from "next"; - import { NextAdmin, AdminComponentProps } from "@premieroctet/next-admin"; - import schema from "./../../prisma/json-schema/json-schema.json"; // import the json-schema.json file - import { PrismaClient } from "@prisma/client"; - import "../../../styles.css" // .css file containing tailiwnd rules - const prisma = new PrismaClient(); + More information about the `createHandler` function [here](/docs/api/create-handler-function). + - export default function Admin(props: AdminComponentProps) { - return ; - } +### Next Admin options - optional - export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const { nextAdminRouter } = await import( - "@premieroctet/next-admin/dist/router" - ); - const adminRouter = await nextAdminRouter(prisma, schema); - return adminRouter.run(req, res) as Promise< - GetServerSidePropsResult<{ [key: string]: any }> - >; - }; - ``` +The `NextAdmin` component accepts an optional `options` prop. In the blocks above, you can see that the `options` prop is commented out. It may be useful to centralize the options in a single file. More information about the options [here](/docs/api/options). -The `nextAdminRouter` function accepts a third optional parameter, which is a Next Admin [options](/docs/api-docs#next-admin-options) object. + + The `options` parameter can be set to function/component, if you are using + options, be sure to pass the same options to the handler and the router + function. + - - + -## Usage +### Usage Once done, you can navigate to the `/admin` route. - -You should be able to see the admin dashboard. diff --git a/apps/docs/pages/docs/glossary.mdx b/apps/docs/pages/docs/glossary.mdx index 44ccfce1..34f3c0a1 100644 --- a/apps/docs/pages/docs/glossary.mdx +++ b/apps/docs/pages/docs/glossary.mdx @@ -1,17 +1,21 @@ +import { Callout } from "nextra/components"; + In this documentation we will use the following conventions: ## List view Refers to the view that displays a table of model's records. +E.g. below is an example of a list view for a `User` model: + +![List view](/docs/list-view.png) + ## Edit view Refers to the view that allows you to edit a single record. -## GUI +E.g. below is an example of an edit view for a `User` model: -Refers to the graphical user interface of the application. +![Edit view](/docs/edit-view.png) -## Schema -Refers to the [prisma schema](https://www.prisma.io/docs/concepts/components/prisma-schema) file that defines the data model of your application. diff --git a/apps/docs/pages/docs/i18n.mdx b/apps/docs/pages/docs/i18n.mdx index 2db764c2..d45eb3c4 100644 --- a/apps/docs/pages/docs/i18n.mdx +++ b/apps/docs/pages/docs/i18n.mdx @@ -1,40 +1,152 @@ +import { Callout } from "nextra/components"; +import OptionsTable from "../../components/OptionsTable"; + # I18n + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs/i18n) + + Next Admin supports i18n with the `translations` prop of the `NextAdmin` component. The following keys are accepted: +The "Showing from" text in the list indicator, e.g: Showing from 1 to 10 of 25, + defaultValue: '"Showing from"', + }, + { + name: 'list.footer.indicator.to', + description: <>The "to" text in the list indicator, e.g: Showing from 1 to 10 of 25, + defaultValue: '"to"', + }, + { + name: 'list.footer.indicator.of', + description: <>The "of" text in the list indicator, e.g: Showing from 1 to 10 of 25, + defaultValue: '"of"', + }, + { + name: 'list.row.actions.delete.label', + description: 'The text in the delete button displayed at the end of each row', + defaultValue: '"Delete"', + }, + { + name: 'list.row.actions.delete.alert', + description: 'The text in the native alert when the delete action is called in the list', + defaultValue: '"Are you sure you want to delete {{count}} element(s)?"', + }, + { + name: 'list.row.actions.delete.success', + description: 'The text appearing after a successful deletion from the list', + defaultValue: '"Deleted successfully"', + }, + { + name: 'list.row.actions.delete.error', + description: 'The text appearing after an error during the deletion from the list', + defaultValue: '"An error occured while deleting"', + }, + { + name: 'list.empty.label', + description: 'The text displayed when there is no row in the list', + defaultValue: '"No {{resource}} found"', + }, + { + name: 'list.empty.caption', + description: 'The caption displayed when there is no row in the list', + defaultValue: '"Get started by creating a new {{resource}}"', + }, + { + name: 'form.button.save.label', + description: 'The text displayed in the form submit button', + defaultValue: '"Submit"', + }, + { + name: 'form.button.delete.label', + description: 'The text displayed in the form delete button', + defaultValue: '"Delete"', + }, + { + name: 'form.delete.alert', + description: 'The text displayed on the alert when the delete button is clicked', + defaultValue: '"Are you sure you want to delete this?"', + }, + { + name: 'form.widgets.file_upload.label', + description: 'The text displayed in file upload widget to select a file', + defaultValue: '"Choose a file"', + }, + { + name: 'form.widgets.file_upload.drag_and_drop', + description: 'The text displayed in file upload widget to indicate a drag & drop is possible', + defaultValue: '"or drag and drop"', + }, + { + name: 'form.widgets.file_upload.delete', + description: 'The text displayed in file upload widget to delete the current file', + defaultValue: '"Delete"', + }, + { + name: 'form.widgets.multiselect.select', + description: 'The text displayed in the multiselect widget in list display mode to toggle the select dialog', + defaultValue: '"Select items"', + }, + { + name: 'actions.label', + description: 'The text displayed in the dropdown button for the actions list', + defaultValue: '"Action"', + }, + { + name: 'actions.edit.label', + description: 'The text displayed for the default edit action in the actions dropdown', + defaultValue: '"Edit"', + }, + { + name: 'actions.create.label', + description: 'The text displayed for the default create action in the actions dropdown', + defaultValue: '"Create"', + }, + { + name: 'actions.delete.label', + description: 'The text displayed for the default delete action in the actions dropdown', + defaultValue: '"Delete"', + }, + { + name: 'selector.loading', + description: 'The text displayed in the selector widget while loading the options', + defaultValue: '"Loading..."', + }, + { + name: 'user.logout', + description: 'The text displayed in the logout button', + defaultValue: '"Logout"', + }, + { + name: 'model', + description: <>Object to custom model and fields names (more details)., + defaultValue: '{}', + }, + ]} +/> + +### Translate model name and fields + +There are two ways to translate these default keys, provide a function named `getMessages` inside the options or provide `translations` props to `NextAdmin` component. -| Name | Description | Default value | -| -------------------------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------------------------- | -| list.header.add.label | The "Add" button in the list header | Add | -| list.header.search.placeholder | The placeholder used in the search input | Search | -| list.footer.indicator.showing | The "Showing from" text in the list indicator, e.g: Showing from 1 to 10 of 25 | Showing from | -| list.footer.indicator.to | The "to" text in the list indicator, e.g: Showing from 1 to 10 of 25 | to | -| list.footer.indicator.of | The "of" text in the list indicator, e.g: Showing from 1 to 10 of 25 | of | -| list.row.actions.delete.label | The text in the delete button displayed at the end of each row | Delete | -| list.row.actions.delete.alert | The text in the native alert when the delete action is called in the list | Are you sure you want to delete \{\{count\}\} element(s)? | -| list.row.actions.delete.success | The text appearing after a successful deletion from the list | Deleted successfully | -| list.row.actions.delete.error | The text appearing after an error during the deletion from the list | An error occured while deleting | -| list.empty.label | The text displayed when there is no row in the list | No \{\{resource\}\} found | -| list.empty.caption | The caption displayed when there is no row in the list | Get started by creating a new \{\{resource\}\} | -| form.button.save.label | The text displayed in the form submit button | Submit | -| form.button.delete.label | The text displayed in the form delete button | Delete | -| form.delete.alert | The text displayed on the alert when the delete button is clicked | Are you sure you want to delete this? | -| form.widgets.file_upload.label | The text displayed in file upload widget to select a file | Choose a file | -| form.widgets.file_upload.drag_and_drop | The text displayed in file upload widget to indicate a drag & drop is possible | or drag and drop | -| form.widgets.file_upload.delete | The text displayed in file upload widget to delete the current file | Delete | -| form.widgets.multiselect.select | The text displayed in the multiselect widget in list display mode to toggle the select dialog | Select items | -| actions.label | The text displayed in the dropdown button for the actions list | Action | -| actions.edit.label | The text displayed for the default edit action in the actions dropdown | Edit | -| actions.create.label | The text displayed for the default create action in the actions dropdown | Create | -| actions.delete.label | The text displayed for the default delete action in the actions dropdown | Delete | -| selector.loading | The text displayed in the selector widget while loading the options | Loading... | -| user.logout | The text displayed in the logout button | Logout | -| model | Object to custom model and fields names. [More details](#translate-model-name-and-fields) | {} | - -There is two ways to translate these default keys, provide a function named `getMessages` inside the options or provide `translations` props to `NextAdmin` component. - -> Note that the function way allows you to provide an object with a multiple level structure to translate the keys, while the `translations` props only allows you to provide a flat object (`form.widgets.file_upload.delete` ex.) + + Using `getMessages` allows you to provide an object with a multiple level structure to translate the keys, while the `translations` props only allow you to provide a flat object (`form.widgets.file_upload.delete` ex.) + You can also pass your own set of translations. For example you can set a custom action name as a translation key, which will then be translated by the lib. @@ -53,23 +165,24 @@ actions: [ ], ``` -Here, the `actions.user.email` key will be translated by the lib, and the value will be used as the action title, aswell as the success and error messages after the action's execution. +Here, the `actions.user.email` key will be translated by the lib, and the value will be used as the action title, as well as the success and error messages after the action's execution. Currently, you can only translate the following: - action title, success and error message - field validation error message -Check the example app for more details on the usage. +Check out the example app for more details on its usage. -## Translate model name and fields +### Translate model name and fields By using the `model` key in the translations object, you can translate the model name and fields. ```json { "model": { - "User": { // Keep the case sensitive name of the model + "User": { + // Keep the case sensitive name of the model "name": "User", "plural": "Users", "fields": { @@ -81,4 +194,6 @@ By using the `model` key in the translations object, you can translate the model } ``` -> Note that if you only use one language for your admin, you should prefer to use the `alias` system ([more details](/docs/api-docs#model)) to customize field names. + + If you only use one language for your admin, you should prefer to use the `alias` system ([more details](/docs/api/model-configuration)) to customize field names. + diff --git a/apps/docs/pages/docs/index.mdx b/apps/docs/pages/docs/index.mdx index 8c6d7631..da82eadb 100644 --- a/apps/docs/pages/docs/index.mdx +++ b/apps/docs/pages/docs/index.mdx @@ -1,12 +1,29 @@ + + +import { + BoltIcon, + CodeBracketIcon, + PlayCircleIcon, +} from '@heroicons/react/24/outline' +import { Callout, Card, Cards } from 'nextra/components' + # 🧞 Next Admin + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs) + -###### `next-admin` is a library built on top of [Prisma](https://www.prisma.io/) and [Next.js](https://nextjs.org/) that allows you to easily manage and visualize your Prisma database in a nice GUI. + + } title="Installation" href="/docs/getting-started" /> + } title="Live demo" href="https://next-admin-po.vercel.app/admin" /> + } title="Source code" href="https://github.com/premieroctet/next-admin" /> + -Get started by following the [installation guide](/docs/getting-started) or check out the [live demo](https://next-admin-po.vercel.app/admin). +###### `next-admin` is a library built on top of [Prisma](https://www.prisma.io/) and [Next.js](https://nextjs.org/) that allows you to easily manage and visualize your Prisma database in a nice graphical user interface (GUI). + +Get started by following the [installation guide](/docs/getting-started), or check out the [live demo](https://next-admin-po.vercel.app/admin). -![Hello](/screenshot.png) -## ✨ Features +![Hello](/screenshot.png) This library is still under development. The following features are available: @@ -17,6 +34,6 @@ This library is still under development. The following features are available: - 🎨 Dashboard widgets and customizable panels - ⚛️ Integration with Prisma ORM - 👔 Customizable list and form -- ⚙️ Supports App Router and Page Router +- ⚙️ Supports App Router and Pages Router If you want to request a feature, please open an issue [here](https://github.com/premieroctet/next-admin/issues/new) diff --git a/apps/docs/pages/docs/route.mdx b/apps/docs/pages/docs/route.mdx index 33e118ff..fb506be1 100644 --- a/apps/docs/pages/docs/route.mdx +++ b/apps/docs/pages/docs/route.mdx @@ -1,10 +1,53 @@ +import { Callout, FileTree, Tabs } from 'nextra/components' + + # Customize the admin route name + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs/route) + + When setting up `next-admin` you can set the admin panel route name to whatever you want. The admin panel route name is set by your folder name. Examples: -`pages/admin/[[...nextadmin]].tsx` will be `/admin` + You want to have your next admin panel at `https://website.com/foo`: + - `apiBasePath` will be `/api/foo` + - and `basePath` will be `/foo` -`pages/prisma/[[...nextadmin]].tsx` will be `/prisma` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/apps/docs/pages/docs/theming.mdx b/apps/docs/pages/docs/theming.mdx index cc6125a8..b2b7573c 100644 --- a/apps/docs/pages/docs/theming.mdx +++ b/apps/docs/pages/docs/theming.mdx @@ -1,5 +1,11 @@ +import { Callout } from "nextra/components"; + # Theming + + This is the documentation for the latest version of Next Admin. If you are using an older version (`<5.0.0`), please refer to the [documentation](/v4/docs/theming) + + Next Admin comes with a preset that is used to theme the different components of the pages. You can add the preset to your Tailwind config presets : diff --git a/apps/docs/pages/docs/v5-migration-guide.mdx b/apps/docs/pages/docs/v5-migration-guide.mdx new file mode 100644 index 00000000..16d0790c --- /dev/null +++ b/apps/docs/pages/docs/v5-migration-guide.mdx @@ -0,0 +1,160 @@ +import { Callout, Steps, Tabs } from 'nextra/components'; +import OptionsTable from '../../components/OptionsTable'; + +## Why migrate? +Next Admin v5 is a major release that comes with a lot of improvements. It is also a breaking change, so you will need to update your code to make it work with the new version. +We decided to make this breaking change to make the library easier to use. +Actions are removed in v5 and replaced by a catch-all API route `[[...nextadmin]]` that handles all the next-admin API routes. + +## How to migrate from v4 to v5 ? + + + ### Upgrade the package + + First, you need to upgrade the package to the latest version. You can do this by running the following command: + + + + ```bash copy + yarn add @premieroctet/next-admin@latest + ``` + + + ```bash copy + npm install -S @premieroctet/next-admin@latest + ``` + + + ```bash copy + pnpm install -S @premieroctet/next-admin@latest + ``` + + + + ### Create the `[[...nextadmin]]` API route + + Next Admin v5 uses a catch-all route `[[...nextadmin]]` to handle all the API routes. You need to create this route in your project. + + + Refer to [this guide](/docs/route) to know where you should create the `[[...nextadmin]]` route. + + + + + ```ts copy filename="app/api/admin/[[...nextadmin]]/route.ts" + import { createHandler } from '@premieroctet/next-admin/dist/appHandler'; + import schema from '@/path/to/json-schema.json'; + import prisma from '@/path/to/prisma/client'; + import { options } from '@/path/to/next-admin-options'; + + const { run } = createHandler({ + apiBasePath: '/api/admin', + prisma, + schema, + options, // optional + }); + + export { run as DELETE, run as GET, run as POST }; + ``` + + + ```ts copy filename="pages/api/admin/[[...nextadmin]].ts" + import { createHandler } from '@premieroctet/next-admin/dist/pageHandler'; + import schema from '@/path/to/json-schema.json'; + import prisma from '@/path/to/prisma/client'; + import { options } from '@/path/to/next-admin-options'; + + export const config = { + api: { + bodyParser: false, + }, + }; + + const { run } = createHandler({ + apiBasePath: '/api/admin', + prisma, + schema, + options, // optional + }); + + export default run; + ``` + + + + + ### Remove `basePath` property + The `options` property is not required anymore, so we moved the `basePath` (required) to the `` component. You can remove it from your `options` object. + + ### Remove server actions + If you were using App Router you had to create server actions to perform some operations. + In v5 these actions are no longer needed. You can remove them from your project. + + ### Update your Next-Admin page + Update your Next-Admin page to use the **new** `getNextAdminProps` **function** (more details [here](/docs/api/get-next-admin-props-function)). + + + + ```ts copy filename="app/admin/[[...nextadmin]]/page.tsx" + import { NextAdmin, PageProps } from '@premieroctet/next-admin'; + import { getNextAdminProps } from '@premieroctet/next-admin/dist/appRouter'; + import prisma from '@/path/to/prisma/client'; + import schema from '@/prisma/json-schema/json-schema.json'; + import { options } from '@/path/to/next-admin-options'; + import '@/styles.css' // .css file containing tailiwnd rules + + export default async function AdminPage({ + params, + searchParams, + }: PageProps) { + const props = await getNextAdminProps({ + params: params.nextadmin, + searchParams, + basePath: '/admin', + apiBasePath: '/api/admin', + prisma, + schema, + options, // optional + }); + + return ; + } + ``` + + + ```ts copy filename="pages/api/admin/[[...nextadmin]].ts" + import { AdminComponentProps, NextAdmin } from '@premieroctet/next-admin'; + import { getNextAdminProps } from '@premieroctet/next-admin/dist/pageRouter'; + import { GetServerSideProps } from 'next'; + import prisma from ' @/path/to/prisma/client'; + import schema from '@/prisma/json-schema/json-schema.json'; + import { options } from '@/path/to/next-admin-options'; + import '@/styles.css'; + + export default function Admin(props: AdminComponentProps) { + return ( + + ); + } + + export const getServerSideProps: GetServerSideProps = async ({ req }) => + await getNextAdminProps({ + basePath: '/admin', + apiBasePath: '/api/admin', + prisma, + schema, + options, // optional + req, + }); + ``` + + + + +You should now have a working Next Admin v5 project. +If you encounter any problems, please refer to the corresponding documentation or open a ticket on our Github repository + + diff --git a/apps/docs/pages/v4/_meta.json b/apps/docs/pages/v4/_meta.json new file mode 100644 index 00000000..459c2983 --- /dev/null +++ b/apps/docs/pages/v4/_meta.json @@ -0,0 +1,7 @@ +{ + "docs": { + "title": "Documentation", + "type": "page", + "display": "hidden" + } +} \ No newline at end of file diff --git a/apps/docs/pages/v4/docs/_meta.json b/apps/docs/pages/v4/docs/_meta.json new file mode 100644 index 00000000..0ed0bfe1 --- /dev/null +++ b/apps/docs/pages/v4/docs/_meta.json @@ -0,0 +1,23 @@ + +{ + "index": "Introduction", + "getting-started": "Getting Started", + "api": { + "title": "API", + "theme": { + "breadcrumb": true, + "footer": true, + "sidebar": true, + "toc": true, + "pagination": true + } + }, + "authentication": "Authentication", + "i18n": "I18n", + + "theming": "Theming", + "glossary": "Glossary", + "route": "Route name", + "edge-cases": "Edge cases", + "code-snippets": "Code snippets" +} diff --git a/apps/docs/pages/v4/docs/api/_meta.json b/apps/docs/pages/v4/docs/api/_meta.json new file mode 100644 index 00000000..bb908cad --- /dev/null +++ b/apps/docs/pages/v4/docs/api/_meta.json @@ -0,0 +1,17 @@ +{ + "next-admin-component": { + "title": "" + }, + "get-props-from-params": { + "title": "getPropsFromParams()" + }, + "next-admin-router": { + "title": "nextAdminRouter()" + }, + "options": { + "title": "Options parameter" + }, + "model-configuration": { + "title": "Model configuration" + } +} diff --git a/apps/docs/pages/v4/docs/api/get-props-from-params.mdx b/apps/docs/pages/v4/docs/api/get-props-from-params.mdx new file mode 100644 index 00000000..59d8ce7a --- /dev/null +++ b/apps/docs/pages/v4/docs/api/get-props-from-params.mdx @@ -0,0 +1,24 @@ +import { Callout } from "nextra/components"; + +# `getPropsFromParams` function + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + + The following function should be used only for App Router. + + +`getPropsFromParams` is a function that returns the props for the [`NextAdmin`](/v4/docs/api/next-admin-component) component. It accepts one argument which is an object with the following properties: + +- `params`: the array of route params retrieved from the [optional catch-all segment](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes#optional-catch-all-segments) +- `searchParams`: the query params [retrieved from the page](https://nextjs.org/docs/app/api-reference/file-conventions/page#searchparams-optional) +- `options`: the [options](/v4/docs/api/options) object +- `schema`: the json schema generated by the `prisma generate` command +- `prisma`: your Prisma client instance +- `action`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to submit the form. It should be your own action, that wraps the `submitForm` action imported from `@premieroctet/next-admin/dist/actions`. +- `deleteAction`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to delete one or more records in a resource. It is optional, and should be your own action. This action takes 3 parameters: `model` (the model name) and `ids` (an array of ids to delete). Next Admin provides a default action for deletion, that you can call in your own action. Check the example app for more details. +- `getMessages`: a function with no parameters that returns translation messages. It is used to translate the default messages of the library. See [i18n](/docs/i18n) for more details. +- `searchPaginatedResourceAction`: the [server action](https://nextjs.org/docs/app/api-reference/functions/server-actions) used to search for resources in a selector widget. This is mandatory for App Router, and will be ignored on page router. Just like `action`, it should be your own action that wraps `searchPaginatedResource` imported from `@premieroctet/next-admin/dist/actions`. \ No newline at end of file diff --git a/apps/docs/pages/v4/docs/api/model-configuration.mdx b/apps/docs/pages/v4/docs/api/model-configuration.mdx new file mode 100644 index 00000000..aef79d64 --- /dev/null +++ b/apps/docs/pages/v4/docs/api/model-configuration.mdx @@ -0,0 +1,587 @@ +import { Callout, Tabs } from "nextra/components"; +import OptionsTable from "../../../../components/OptionsTable"; + +# Model configuration + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + +To configure your models, you can use the `model` property of the [NextAdmin options](/v4/docs/api/options) parameter. + +This property allows you to configure everything about how your models, their fields, their relations are displayed and edited. + +Example for a schema with a `User`, `Post` and `Category` model: + +```tsx copy +import { NextAdminOptions } from "@premieroctet/next-admin"; + +export const options: NextAdminOptions = { + /* Your other options here */ + model: { + User: { + toString: (user) => `${user.name} (${user.email})`, + title: "Users", + icon: "UsersIcon", + list: { + display: ["id", "name", "email", "posts", "role", "birthDate"], + search: ["name", "email"], + filters: [ + { + name: "is Admin", + active: false, + value: { + role: { + equals: "ADMIN", + }, + }, + }, + ], + }, + edit: { + display: [ + "id", + "name", + "email", + "posts", + "role", + "birthDate", + "avatar", + ], + }, + actions: [ + { + title: "Send email", + action: async (model, ids) => { + const response = await fetch("/api/email", { + method: "POST", + body: JSON.stringify(ids), + }); + + if (!response.ok) { + throw new Error("Failed to send email"); + } + }, + successMessage: "Email sent successfully", + errorMessage: "Error while sending email", + }, + ], + }, + Post: { + toString: (post) => `${post.title}`, + }, + Category: { + title: "Categories", + icon: "InboxStackIcon", + toString: (category) => `${category.name}`, + list: { + display: ["name", "posts"], + }, + edit: { + display: ["name", "posts"], + }, + }, + }, +}; + +``` +## `model` + +It takes as **key** a model name of your schema as **value** an object to configure it. + +By default if no models are defined, they will all be displayed in the admin. If you want more control, you have to define each model individually as empty objects or with the following properties: + +an object containing the list options (see list property) + }, + { + name: "edit", + type: "Object", + description: <>an object containing the edit options (see edit property) + }, + { + name: "actions", + type: "Array", + description: <> an array of actions (see actions property) + }, + { + name: "icon", + type: "String", + description: <>the outline HeroIcon name displayed in the sidebar and pages title, + }, + { + name: "permissions", + type: "Array", + description: "an array to specify restricted permissions on model", + }, + ]} /> + +## `list` property +This property determines how your data is displayed in the [list view](/v4/docs/glossary#list-view) + +an object containing the model fields as keys, and customization values (see fields property), + }, + { + name: "copy", + type: "Array", + description: "an array of fields that are copyable into the clipboard", + defaultValue: "undefined - no field is copyable by default", + }, + { + name: "defaultSort", + type: "Object", + description: "an optional object to determine the default sort to apply on the list", + }, + { + name: "defaultSort.field", + type: "String", + description: "the model's field name to which the sort is applied. It is mandatory", + }, + { + name: "defaultSort.direction", + type: "String", + description: "the sort direction to apply. It is optional", + }, + { + name: "filters", + type: "Array", + description: <> define a set of Prisma filters that user can choose in list (see filters), + }, + { + name: "exports", + type: "Object", + description: <>an object or array of export - containing export url (see exports), + }, + ]} +/> + + The `search` property is only available for [`scalar` fields](https://www.prisma.io/docs/orm/reference/prisma-schema-reference#model-field-scalar-types). + + +#### `list.filters` property + +The `filters` property allow you to define a set of [Prisma filters](https://www.prisma.io/docs/orm/prisma-client/queries/filtering-and-sorting) that users can apply on the [list view](/v4/docs/glossary#list-view). It's an array of the following object: + +a where clause Prisma filter of the related model (e.g. Prisma operators), + }, + ]} +/> + +#### `list.exports` property + +The `exports` property allows you to define a set of export actions that users can apply on the [list view](/v4/docs/glossary#list-view). +It's an object or an array of objects that can have the following properties: + + + + + The `exports` property does not account for active filters. If you want to export filtered data, you need to add the filters to the URL or in your export action. + + + +## `edit` property + +This property determines how your data is displayed in the [edit view](/v4/docs/glossary#edit-view) + + an array of fields that are displayed in the form. It can also be an object that will be displayed in the form of a notice (see notice), + defaultValue: "all scalar fields are displayed", + }, + { + name: "styles", + type: "Object", + description: <>an object containing the styles of the form (see styles) , + }, + { + name: "fields", + type: "Object", + description: <>an object containing the model fields as keys, and customization values (see fields property), + }, + { + name: "submissionErrorMessage", + type: "String", + description: "a message displayed if an error occurs during the form submission", + defaultValue: "'Submission error'", + }, +]} + /> + +#### `edit.styles` property + +The `styles` property is available in the `edit` property. + + If your options are defined in a separate file, make sure to add the path to the `content` property of the `tailwind.config.js` file (see [TailwindCSS configuration](/docs/getting-started#tailwindcss-configuration)). + + + + +Here is an example of using `styles` property: + +```ts copy +styles: { + _form: "form-classname", + ... // all fields +}; +``` + +#### `edit.fields` & `list.fields` property + +The `fields` property is available in both `list` and `edit` properties. + + + For the `edit` property, it can take the following: + a string defining an OpenAPI field format, overriding the one set in the generator. An extra file format can be used to be able to have a file input, + }, + { + name: "input", + type: "React Element", + description: <>a React Element that should receive CustomInputProps. For App Router, this element must be a client component, + }, + { + name: "handler", + type: "Object", + description: "", + }, + { + name: "handler.get", + type: "Function", + description: "a function that takes the field value as a parameter and returns a transformed value displayed in the form", + }, + { + name: "handler.upload", + type: "Function", + description: <>an async function that is used only for formats file and data-url. It takes a Buffer object and an information object containing name and type properties as parameters and must return a string. It can be useful to upload a file to a remote provider, + }, + { + name: "handler.uploadErrorMessage", + type: "String", + description: "an optional string displayed in the input field as an error message in case of a failure during the upload handler", + }, + { + name: "optionFormatter", + type: "Function", + description: "only for relation fields, a function that takes the field values as a parameter and returns a string. Useful to display your record in related list", + }, + { + name: "tooltip", + type: "String", + description: "A tooltip content to display for the field ", + }, + { + name: "helperText", + type: "String", + description: "a helper text that is displayed underneath the input", + }, + { + name: "disabled", + type: "Boolean", + description: "a boolean to indicate that the field is read only", + }, + { + name: "display", + type: "String", + description: <> only for relation fields, indicate which display format to use between list, table or select, + defaultValue: "select", + }, + { + name: "required", + type: "Boolean", + description: <>a true value to force a field to be required in the form, note that if the field is required by the Prisma schema, you cannot set required to false, + }, + { + name: "relationOptionFormatter", + type: "Function", + description: <>same as optionFormatter, but used to format data that comes from an explicit many-to-many relationship, + }, + { + name: "orderField", + type: "String", + description: <>the field to use for relationship sorting. This allows to drag and drop the related records in the list display, + }, + { + name: "relationshipSearchField", + type: "String", + description: <>a field name of the explicit many-to-many relation table to apply the search on. See handling explicit many-to-many, + }, + ]} + /> + + + For the `list` property, it can take the following: + a function that takes the field value as a parameter, and returns a JSX node. It also accepts a second argument which is the NextAdmin context, + }, + { + name: "sortBy", + type: "String", + description: "available only on many-to-one models. The name of a field in the related model to apply the sort to. Defaults to the id field of the related model", + }, + ]} + /> + + + + +## `actions` property +The `actions` property is an array of objects that allows you to define a set of actions that can be executed on one or more records of the resource. On the list view, there is a default action for deletion. The object can have the following properties: + +an async function that will be triggered when selecting the action in the dropdown. For App Router, it must be defined as a server action, + }, + { + name: "successMessage", + type: "String", + description: "a message that will be displayed when the action is successful", + }, + { + name: "errorMessage", + type: "String", + description: "a message that will be displayed when the action fails", + }, + ]} +/> + + + +## CustomInputProps type +Represents the props that are passed to the custom input component. + a function taking a ChangeEvent as a parameter, + }, + { + name: "readonly", + type: "Boolean", + description: "boolean indicating if the field is editable or not", + }, + { + name: "rawErrors", + type: "Array", + description: "array of strings containing the field errors", + }, + { + name: "disabled", + type: "Boolean", + description: "boolean indicating if the field is disabled", + }, + ]} +/> + +## NextAdmin Context type +Represents the context that is passed to the formatter function in the [list.fields](#editfields--listfields-property) property. +The locale used by the calling page. (refers to the accept-language header), + }, + { + name: "row", + type: "Object", + description: "The current row of the list view", + }, + ]} +/> + +## Notice type +This type represents the notice object that can be used in the `edit` property of a model. +If you pass a Notice object in the `display` array, it will be displayed in the form. +This can be useful to display alerts or information to the user when editing a record. +The object can have the following properties: + +A unique identifier for the notice that can be used to style it with the styles property. This is mandatory, + }, + { + name: "description", + type: "String", + description: "The description of the notice. This is optional", + }, + ]} +/> + +Here is a quick example of usage. + +Considering you have a model `User` with the following configuration: + +```typescript filename=".../options.ts" copy {13-17} +export const options: NextAdminOptions = { + basePath: "/admin", + title: "⚡️ My Admin", + model: { + User: { + /** + ...some configuration + **/ + edit: { + display: [ + "id", + "name", + { + title: "Email is mandatory", + id: "email-notice", + description: "You must add an email from now on", + } as const, + "email", + "posts", + "role", + "birthDate", + "avatar", + "metadata", + ], + styles: { + _form: "grid-cols-3 gap-4 md:grid-cols-4", + id: "col-span-2 row-start-1", + name: "col-span-2 row-start-1", + "email-notice": "col-span-4 row-start-3", + email: "col-span-4 md:col-span-2 row-start-4", + posts: "col-span-4 md:col-span-2 row-start-5", + role: "col-span-4 md:col-span-2 row-start-6", + birthDate: "col-span-3 row-start-7", + avatar: "col-span-4 row-start-8", + metadata: "col-span-4 row-start-9", + }, + /** ... some configuration */ + }, + }, + }, +}; +``` + +In this example, the `email-notice` notice will be displayed in the form before the `email` field. +![Notice example](/docs/notice-exemple.png) + diff --git a/apps/docs/pages/v4/docs/api/next-admin-component.mdx b/apps/docs/pages/v4/docs/api/next-admin-component.mdx new file mode 100644 index 00000000..5ddbe28c --- /dev/null +++ b/apps/docs/pages/v4/docs/api/next-admin-component.mdx @@ -0,0 +1,94 @@ +import { Callout, Tabs } from "nextra/components"; + +# `` + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + +`` is a React component that contains the entire UI of Next Admin. It can take several props: +- `options`: used to customize the UI, like field formatters for example. Do not use with App router (see [Options](/v4/docs/api/options) for more details). +- `dashboard`: used to customize the rendered dashboard. You can provide your own dashboard component. Dashboard is rendered in your base url (e.g. `/admin`). +- `translations`: used to customize some of the texts displayed in the UI. See [i18n](/v4/docs/i18n) for more details. +- `user`: used to add some user information at the bottom of the menu. See [user](/v4/docs/api/user) for more details. +- Some other props that are used internally by Next Admin that are not documented here and that you should pass as is. Depending on the type of Next Router you're using (App or Pages) you will get these props differently. + + + Do not override any `AdminComponentProps` props that are not documented here. They are used internally by Next Admin. You can only override these props: + `options` / `dashboard` / `translations` / `user`, and they're optional. + + +This is an example using the `NextAdmin` component with a custom Dashboard component and options: + + + ```tsx copy filename="app/admin/[[...nextadmin]]/page.tsx" + import Dashboard from "path/to/CustomDashboard"; + import { options } from "@/options"; + + export default async function AdminPage({ + params, + searchParams, + }: { + params: { [key: string]: string[] | string }; + searchParams: { [key: string]: string | string[] | undefined } | undefined; + }) { + const props = await getPropsFromParams({ + params: params.nextadmin as string[], + searchParams, + options, + prisma, + schema, + }); + + return ( + + ); + } + ``` + + Passing the `options` prop like you'd do on Pages router will result in an error in case you + have functions defined inside the options object (formatter, handlers, etc.). You should pass the options object in the `getPropsFromParams` function only. + + + + ```tsx copy filename="pages/admin/[[...nextadmin]].tsx" + import Dashboard from "path/to/CustomDashboard"; + import { options } from "path/to/options"; + + export default function Admin(props: AdminComponentProps) { + return ( + + ); + } + + + export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const { nextAdminRouter } = await import( + "@premieroctet/next-admin/dist/router" + ); + + const adminRouter = await nextAdminRouter(prisma, schema, options); + return adminRouter.run(req, res) as Promise< + GetServerSidePropsResult<{ [key: string]: any }> + >; + }; + + ``` + + In the Pages Router, `options` parameter needs to be passed in the `` component. But also as a third argument to the `nextAdminRouter` function. + + + \ No newline at end of file diff --git a/apps/docs/pages/v4/docs/api/next-admin-router.mdx b/apps/docs/pages/v4/docs/api/next-admin-router.mdx new file mode 100644 index 00000000..c577ca8a --- /dev/null +++ b/apps/docs/pages/v4/docs/api/next-admin-router.mdx @@ -0,0 +1,50 @@ +import { Callout } from "nextra/components"; + +# `nextAdminRouter` function + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + + + The following function should be used only for Pages Router. + + +`nextAdminRouter` is a function that returns a promise of a _Node Router_ that you can use in your getServerSideProps function to start using Next Admin. Its usage is only related to Pages Router. + +Usage example: + +```ts copy filename="pages/api/admin/[[...nextadmin]].ts" +export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const { nextAdminRouter } = await import( + "@premieroctet/next-admin/dist/router" + ); + const adminRouter = await nextAdminRouter(prisma, schema); + return adminRouter.run(req, res) as Promise< + GetServerSidePropsResult<{ [key: string]: any }> + >; +}; +``` + +It takes 3 parameters: + +- Your Prisma client instance, _required_ +- Your Prisma schema, _required_ + +- and an _optional_ object of type [`NextAdminOptions`](/v4/docs/api/options) to customize your admin with the following properties: + +```ts +import { NextAdminOptions } from "@premieroctet/next-admin"; + +const options: NextAdminOptions = { + model: { + User: { + toString: (user) => `${user.email} / ${user.name}`, + }, + }, +}; + +const adminRouter = await nextAdminRouter(prisma, schema, options); +``` \ No newline at end of file diff --git a/apps/docs/pages/v4/docs/api/options.mdx b/apps/docs/pages/v4/docs/api/options.mdx new file mode 100644 index 00000000..7cb03641 --- /dev/null +++ b/apps/docs/pages/v4/docs/api/options.mdx @@ -0,0 +1,213 @@ +import { Callout, Tabs } from "nextra/components"; +import OptionsTable from "../../../../components/OptionsTable"; + +# Next Admin options parameter + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + +Next Admin `options` is a parameter that allows you to configure your admin panel to your needs. + + +Refers to [this documentation](/v4/docs/api/next-admin-component) to see where you should use the options parameters. Wether you're using the App Router or the Pages Router, the options object is the same. But the way you pass it to `` is different. + + +Example: + +```tsx copy +import { NextAdminOptions } from "@premieroctet/next-admin"; + +export const options: NextAdminOptions = { + basePath: "/admin", + title: "⚡️ My Admin Page", + model: { + /* Your model configuration here */ + }, + pages: { + "/custom": { + title: "Custom page", + icon: "AdjustmentsHorizontalIcon", + }, + }, + externalLinks: [ + { + label: "App Router", + url: "/ ", + }, + ], + sidebar: { + groups: [ + { + title: "Users", + models: ["User"], + }, + { + title: "Categories", + models: ["Category"], + }, + ], + }, +}; +``` +--- +The `NextAdminOptions` type is an object that can have the following properties: + +## `title` + The title of the admin panel. It is displayed in the browser tab and in the sidebar menu. + +## `basePath` + +`basePath` is a string that represents the base path of your admin. (e.g. `/admin`) - optional. + +## `model` + +`model` is a property that allows you to configure everything about how your models, their fields, their relations are displayed and edited. It's highly configurable you can learn more about it [here](/v4/docs/api/model-configuration). + +## `pages` + +`pages` is an object that allows you to add your own sub-pages as a sidebar menu entry. It is an object where the key is the path (without the base path) and the value is an object with the following properties: + +The outline HeroIcon name of the page displayed on the sidebar, + }, + ]} +/> + + + +## `sidebar` + +The `sidebar` property allows you to customize the aspect of the sidebar menu. It is an object that can have the following properties: + + + + +## `externalLinks` + +The `externalLinks` property allows you to add external links to the sidebar menu. It is an array of objects that can have the following properties: + + + + +## `defaultColorScheme` + +The `defaultColorScheme` property defines a default color palette between `light`, `dark` and `system`, but allows the user to modify it. Default to `system`. + +## `forceColorScheme` + +Identical to `defaultColorScheme` but does not allow the user to change it. + +Here is an example of using `NextAdminOptions` for the following schema : + +```prisma copy filename="schema.prisma" +enum Role { + USER + ADMIN +} + +model User { + id Int @id @default(autoincrement()) + email String @unique + name String? + password String @default("") + posts Post[] @relation("author") // One-to-many relation + profile Profile? @relation("profile") // One-to-one relation + birthDate DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + role Role @default(USER) +} +``` + +```tsx copy filename=" pages/api/admin/[[...nextadmin]].ts" +const options: NextAdminOptions = { + basePath: "/admin", + model: { + User: { + toString: (user) => `${user.name} (${user.email})`, + list: { + display: ["id", "name", "email", "posts", "role", "birthDate"], + search: ["name", "email"], + fields: { + role: { + formatter: (role) => { + return {role.toString()}; + }, + }, + birthDate: { + formatter: (date) => { + return new Date(date as unknown as string) + ?.toLocaleString() + .split(" ")[0]; + }, + }, + }, + }, + edit: { + display: ["id", "name", "email", "posts", "role", "birthDate"], + fields: { + email: { + validate: (email) => email.includes("@") || "Invalid email", + }, + birthDate: { + format: "date", + }, + avatar: { + format: "file", + handler: { + upload: async (buffer, infos) => { + return "https://www.gravatar.com/avatar/00000000000000000000000000000000"; + }, + }, + }, + }, + }, + }, + }, +}; + +const adminRouter = await nextAdminRouter(prisma, schema, options); +``` \ No newline at end of file diff --git a/apps/docs/pages/v4/docs/api/user.mdx b/apps/docs/pages/v4/docs/api/user.mdx new file mode 100644 index 00000000..34fb21c9 --- /dev/null +++ b/apps/docs/pages/v4/docs/api/user.mdx @@ -0,0 +1,33 @@ +import { Callout, Tabs } from "nextra/components"; +import OptionsTable from "../../../../components/OptionsTable"; + +# User + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + +The `user` property allows you to define the user displayed in the sidebar menu. +Its an object that can have the following properties: + + \ No newline at end of file diff --git a/apps/docs/pages/v4/docs/authentication.mdx b/apps/docs/pages/v4/docs/authentication.mdx new file mode 100644 index 00000000..515382c3 --- /dev/null +++ b/apps/docs/pages/v4/docs/authentication.mdx @@ -0,0 +1,77 @@ +import { Callout, Tabs } from "nextra/components"; + +# Authentication + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + + + + The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check in the page: + + + The following example uses [next-auth](https://next-auth.js.org/) to handle authentication + + + ```ts copy filename="app/admin/[[...nextadmin]]/page.tsx" {8-9, 11-13} + export default async function AdminPage({ + params, + searchParams, + }: { + params: { [key: string]: string[] }; + searchParams: { [key: string]: string | string[] | undefined } | undefined; + }) { + const session = await getServerSession(authOptions); + const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check + + if (!isAdmin) { + redirect('/', { permanent: false }) + } + + const props = await getPropsFromParams({ + params: params.nextadmin, + searchParams, + options, + prisma, + schema, + action: submitFormAction, + }); + + return ; + } + ``` + + + + The library does not provide an authentication system. If you want to add your own, you can do so by adding a role check to the `getServerSideProps` function: + + + The following example uses [next-auth](https://next-auth.js.org/) to handle authentication + + + ```ts copy filename="pages/api/admin/[[...nextadmin]].ts" {2-3, 5-12} + export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const session = await getServerSession(req, res, authOptions); + const isAdmin = session?.user?.role === "SUPERADMIN"; // your role check + + if (!isAdmin) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + + const { nextAdminRouter } = await import( + "@premieroctet/next-admin/dist/nextAdminRouter" + ); + return nextAdminRouter(client).run(req, res); + }; + ``` + + + \ No newline at end of file diff --git a/apps/docs/pages/v4/docs/code-snippets.mdx b/apps/docs/pages/v4/docs/code-snippets.mdx new file mode 100644 index 00000000..3af2b19b --- /dev/null +++ b/apps/docs/pages/v4/docs/code-snippets.mdx @@ -0,0 +1,232 @@ +import { Callout, FileTree, Tabs } from 'nextra/components' + +# Code snippets + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + +This page contains code snippets that you can use in your projects. These are not a part of Next Admin, but may be useful for your projects. + +Some of the snippets are implemented in the example project. You can check them out in the [example project](https://next-admin-po.vercel.app) or in the [source code](https://github.com/premieroctet/next-admin/tree/main/apps/example). + +## Export data + +By using [exports](/v4/docs/api/model-configuration#listexports-property) options, you can export data. Next Admin only implements the CSV export button, it's actually a link pointing to a provided url. + +The API endpoint route must be defined in the `exports` property of the options object. This is an example of how to implement the export API endpoint: + +```typescript copy filename="app/api/users/export/route.ts" +import { prisma } from "@/prisma"; + +export async function GET() { + const users = await prisma.user.findMany(); + const csv = users.map((user) => { + return `${user.id},${user.name},${user.email},${user.role},${user.birthDate}`; + }); + + const headers = new Headers(); + headers.set("Content-Type", "text/csv"); + headers.set("Content-Disposition", `attachment; filename="users.csv"`); + + return new Response(csv.join("\n"), { + headers, + }); +} +``` + +or with a stream response: + +```typescript copy filename="app/api/users/export/route.ts" +import { prisma } from "@/prisma"; +const BATCH_SIZE = 1000; + +export async function GET() { + const headers = new Headers(); + headers.set("Content-Type", "text/csv"); + headers.set("Content-Disposition", `attachment; filename="users.csv"`); + + const stream = new ReadableStream({ + async start(controller) { + try { + const batchSize = BATCH_SIZE; + let skip = 0; + let users; + + do { + users = await prisma.user.findMany({ + skip, + take: batchSize, + }); + + const csv = users + .map((user) => { + return `${user.id},${user.name},${user.email},${user.role},${user.birthDate}\n`; + }) + .join(""); + + controller.enqueue(Buffer.from(csv)); + + skip += batchSize; + } while (users.length === batchSize); + } catch (error) { + controller.error(error); + } finally { + controller.close(); + } + }, + }); + + return new Response(stream, { headers }); +} +``` + +> Note that you must secure the export route if you don't want to expose your data to the public by adding authentication middleware. + +There are two example files in the example project: + +- [app/api/users/export/route.ts](https://github.com/premieroctet/next-admin/tree/main/apps/example/app/api/users/export/route.ts) +- [app/api/posts/export/route.ts](https://github.com/premieroctet/next-admin/tree/main/apps/example/app/api/posts/export/route.ts) + +## Add data to formData before submitting + +If you want to add data to the form data before submitting it, you can add logic to the `submitFormAction` function. This is an example of how to add `createdBy` and `updatedBy` fields based on the user id: + +```typescript copy filename="actions/nextadmin.ts" +"use server"; +import { ActionParams } from "@premieroctet/next-admin"; +import { submitForm } from "@premieroctet/next-admin/dist/actions"; + +export const submitFormAction = async ( + params: ActionParams, + formData: FormData +) => { + const userId = /* get the user id */; + if (params.params[1] === "new") { + formData.append("createdBy", userId); + } else { + formData.append("updatedBy", userId); + } + + return submitForm({ ...params, options, prisma }, formData); +}; +``` + +> Note that this example assumes that you have a `createdBy` and `updatedBy` field on each model, if you need to check the model name, you can use `params.params[0]`. + +This snippet is not implemented in the example project. + +## Custom input form + +If you want to customize the input form, you can create a custom input component. This is an example of how to create a custom input component for the `birthDate` field: + +```typescript copy filename="/components/inputs/BirthDateInput.tsx" +"use client"; +import { CustomInputProps } from "@premieroctet/next-admin"; +import DateTimePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; + +type Props = CustomInputProps; + +const BirthDateInput = ({ value, name, onChange, disabled, required }: Props) => { + return ( + <> + + onChange?.({ + // @ts-expect-error + target: { value: date?.toISOString() ?? new Date().toISOString() }, + }) + } + showTimeSelect + dateFormat="dd/MM/yyyy HH:mm" + timeFormat="HH:mm" + wrapperClassName="w-full" + disabled={disabled} + required={required} + className="dark:bg-dark-nextadmin-background-subtle dark:ring-dark-nextadmin-border-strong text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted ring-nextadmin-border-default focus:ring-nextadmin-brand-default dark:focus:ring-dark-nextadmin-brand-default block w-full rounded-md border-0 px-2 py-1.5 text-sm shadow-sm ring-1 ring-inset transition-all duration-300 placeholder:text-gray-400 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:leading-6 [&>div]:border-none" + /> + + + ); +}; + +export default BirthDateInput; +``` + +The `CustomInputProps` type is provided by Next Admin. + +> Note that we use a hidden input to store the value because the `DateTimePicker` component needs a different value format than what is expected by the form submission. + +You can find an example of this component in the example project: + +- [app/components/inputs/DatePicker.tsx](https://github.com/premieroctet/next-admin/blob/main/apps/example/components/DatePicker.tsx) + +# Explicit many-to-many + +You might want to add sorting on a relationship, for example sort the categories of a post in a specific order. To achieve this, you have to explicitly define a model in the Prisma schema that will act as the join table. This is an example of how to implement this: + +```prisma {8,16,21,22,23,24,25,26,27,28,29,30} +model Post { + id Int @id @default(autoincrement()) + title String + content String? + published Boolean @default(false) + author User @relation("author", fields: [authorId], references: [id]) // Many-to-one relation + authorId Int + categories CategoriesOnPosts[] + rate Decimal? @db.Decimal(5, 2) + order Int @default(0) +} + +model Category { + id Int @id @default(autoincrement()) + name String + posts CategoriesOnPosts[] + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt +} + +model CategoriesOnPosts { + id Int @default(autoincrement()) + post Post @relation(fields: [postId], references: [id]) + postId Int + category Category @relation(fields: [categoryId], references: [id]) + categoryId Int + order Int @default(0) + + @@id([postId, categoryId]) +} +``` + +In the Next Admin options, you will then need to define, on a specific field, which field in the join table will be used for sorting: + +```typescript {11,12} copy +{ + model: { + Post: { + edit: { + fields: { + categories: { + relationOptionFormatter: (category) => { + return `${category.name} Cat.${category.id}`; + }, + display: "list", + orderField: "order", // The field used in CategoriesOnPosts for sorting + relationshipSearchField: "category", // The field to use in CategoriesOnPosts + }, + } + } + } + } +} +``` + +Note that you will need to use `relationOptionFormatter` instead of `optionFormatter` to format the content of the select input. + +With the `list` display, if the `orderField` property is defined, you will be able to apply drag and drop sorting on the categories. Upon form submission, the order will be updated accordingly, starting from 0. + +> The `orderField` property can also be applied for one-to-many relationships. In that case, drag and drop will also be available. diff --git a/apps/docs/pages/v4/docs/edge-cases.mdx b/apps/docs/pages/v4/docs/edge-cases.mdx new file mode 100644 index 00000000..3cdabae1 --- /dev/null +++ b/apps/docs/pages/v4/docs/edge-cases.mdx @@ -0,0 +1,64 @@ +import { Callout, FileTree, Tabs } from 'nextra/components' + +# Edge cases + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + +In this part, we will talk about the edge cases we found during the development of the project. Prisma allows different structures to define models and relations. +We had to choose which to exploit and which to avoid. Some choices may evolve in the future. + +## Fields + +### `id` for identification + +We decided to use the `id` field to identify the models. This field is automatically generated by Prisma and is unique. The library doesn't support Prisma's composite keys. + +So this is the recommended way to identify models. + +```prisma +model User { + id Int @id @default(autoincrement()) + [...] +} +``` + +### Autogenerated fields + +Prisma allows fields to be generated automatically. For example, the `id` field is automatically generated by Prisma. These fields are not editable by the user, neither during creation nor during an update. + + +## Relations + +### One to one + +Prisma allows one-to-one relations. But just one of the two models can have a relation field. If you want to remove the relation, you have to remove the field from the model that doesn't have the relation field. + +There is an example of a one-to-one relation between `User` and `Profile` models. + +```prisma +model User { + id Int @id @default(autoincrement()) + profile Profile? @relation("profile") +} + +model Profile { + id Int @id @default(autoincrement()) + bio String? + user User? @relation("profile", fields: [userId], references: [id]) + userId Int? @unique +} +``` + +Even if the `userId` field is not required, you cannot remove the relationship between `User` and `Profile` without removing the `profile` field from the `user` model. + +### Many to many + +Prisma allows two types of [many-to-many](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/many-to-many-relations) relationships: implicit and explicit. Both are supported in the library. But for better understanding in the Next Admin UI, we recommend using implicit relations. + +### Self relation + +Prisma allows [self relations](https://www.prisma.io/docs/concepts/components/prisma-schema/relations/self-relations). All the self relations are supported by the library. \ No newline at end of file diff --git a/apps/docs/pages/v4/docs/getting-started.mdx b/apps/docs/pages/v4/docs/getting-started.mdx new file mode 100644 index 00000000..300f2fe2 --- /dev/null +++ b/apps/docs/pages/v4/docs/getting-started.mdx @@ -0,0 +1,270 @@ +import { Callout, Steps, Tabs } from "nextra/components"; + +# Getting Started + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs/getting-started). + + + + next-admin is a tool that works only with **Prisma** (for now) so in this guide we will assume you already have a Next.js app with Prisma set up. + +To get started with next-admin follow the steps below. + + + + ### Install next-admin and prisma-json-schema-generator + + + ```bash copy + yarn add @premieroctet/next-admin prisma-json-schema-generator + ``` + + + + ```bash copy + npm install -S @premieroctet/next-admin prisma-json-schema-generator + ``` + + + ```bash copy + pnpm install -S @premieroctet/next-admin prisma-json-schema-generator + ``` + + + + ### Setup TailwindCSS + Next-Admin relies on [TailwindCSS](https://tailwindcss.com/) for the style. If you do not have it, you can [install TailwindCSS](https://tailwindcss.com/docs/installation) with the following config : + + ```typescript copy filename="tailwind.config.js" + module.exports = { + content: [ + "./node_modules/@premieroctet/next-admin/dist/**/*.{js,ts,jsx,tsx}", + ], + darkMode: "class", + presets: [require("@premieroctet/next-admin/dist/preset")], + }; + ``` + + ### Setup SuperJson + SuperJson is required to avoid errors related to invalid serialization properties that can occur when passing data from server to client. + + #### With Babel + + + ```bash copy + yarn add -D babel-plugin-superjson-next superjson@^1 + ``` + + + + ```bash copy + npm install --save-dev babel-plugin-superjson-next superjson@^1 + ``` + + + ```bash copy + pnpm install -D babel-plugin-superjson-next superjson@^1 + ``` + + + Add the `babel-plugin-superjson-next` plugin to your `.babelrc` file: + + ```json copy + { + "presets": ["next/babel"], + "plugins": ["superjson-next"] + } + ``` + + #### With SWC (Experimental) + + + + ```bash copy + yarn add -E -D next-superjson-plugin@0.6.1 superjson + ``` + + + + ```bash copy + npm install --save-dev -E next-superjson-plugin@0.6.1 superjson + ``` + + + ```bash copy + pnpm install -E -D next-superjson-plugin@0.6.1 superjson + ``` + + + + Add the `next-superjson-plugin` plugin to your `next.config.js` file: + + ```js copy filename="next.config.js" + module.exports = { + // your current config + experimental: { + swcPlugins: [ + [ + "next-superjson-plugin", + { + excluded: [], + }, + ], + ], + }, + }; + ``` + + + ### Generate Prisma JSON schema + + Next-Admin relies on [Prisma](https://www.prisma.io/). If you do not have it, you can [install Prisma](https://www.prisma.io/docs/getting-started/setup-prisma/start-from-scratch-typescript-postgres). + + + Add the `prisma-json-schema-generator` generator to your `schema.prisma` file: + + ```prisma copy filename="schema.prisma" + generator jsonSchema { + provider = "prisma-json-schema-generator" + includeRequiredFields = "true" + } + ``` + + Then run the following command : + + ```bash copy + yarn run prisma generate + ``` + + This will create a `json-schema.json` file in the `prisma/json-schema` directory. + + ### Create the Next-Admin page + + First, create an `options.ts` file (see [Options parameter](/v4/docs/api/options) for more details): + + ```tsx copy filename="options.ts" + import { NextAdminOptions } from "@premieroctet/next-admin"; + + export const options: NextAdminOptions = { + basePath: "/admin", + title: "⚡️ My Admin", + } + ``` + + + + Then create an `nextadmin.ts` action file: + ```tsx copy filename="app/actions/nextadmin.ts" + import { ActionParams, ModelName } from "@premieroctet/next-admin"; + import { + SearchPaginatedResourceParams, + deleteResourceItems, + searchPaginatedResource, + submitForm, + } from "@premieroctet/next-admin/dist/actions"; + import { options } from "path/to/options"; + import { prisma } from "path/to/prisma"; + + const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); + + export const submitFormAction = async ( + params: ActionParams, + formData: FormData + ) => { + return submitForm({ ...params, options, prisma }, formData); + }; + + export const deleteItem = async ( + model: ModelName, + ids: string[] | number[] + ) => { + return deleteResourceItems(prisma, model, ids); + }; + + export const searchResource = async ( + actionParams: ActionParams, + params: SearchPaginatedResourceParams + ) => { + return searchPaginatedResource({ ...actionParams, options, prisma }, params); + }; + + ``` + + Finally, create the `page.tsx` file: + + ```tsx copy filename="app/admin/[[...nextadmin]]/page.tsx" + import { NextAdmin } from "@premieroctet/next-admin"; + import { getPropsFromParams } from "@premieroctet/next-admin/dist/appRouter"; + import { prisma } from "path/to/prisma"; + import schema from "path/to/prisma/json-schema/json-schema.json"; + import { submitFormAction, deleteItem, searchResource } from "path/to/actions/nextadmin"; + import "path/to/styles.css"; // .css file containing tailiwnd rules + import { options } from "path/to/options"; + + export default async function AdminPage({ + params, + searchParams, + }: { + params: { [key: string]: string[] }; + searchParams: { [key: string]: string | string[] | undefined } | undefined; + }) { + const props = await getPropsFromParams({ + params: params.nextadmin, + searchParams, + options, + prisma, + schema, + action: submitFormAction, + deleteAction: deleteItem, + searchPaginatedResourceAction: searchResource, + }); + + return ; + } + ``` + + + Make sure to not use `"use client"` in the page. + + + + + + Then create the catch-all route file `[[...nextadmin]].tsx`: + + ```tsx copy filename="pages/admin/[[...nextadmin]].tsx" + import { GetServerSideProps, GetServerSidePropsResult } from "next"; + import { NextAdmin, AdminComponentProps } from "@premieroctet/next-admin"; + import schema from "path/to/prisma/json-schema/json-schema.json"; // import the json-schema.json file + import { options } from "path/to/options"; + import { PrismaClient } from "@prisma/client"; + import "path/to/styles.css"; // .css file containing tailiwnd rules + + const prisma = new PrismaClient(); + + export default function Admin(props: AdminComponentProps) { + return ; + } + + export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { + const { nextAdminRouter } = await import( + "@premieroctet/next-admin/dist/router" + ); + const adminRouter = await nextAdminRouter(prisma, schema, options) + return adminRouter.run(req, res) as Promise< + GetServerSidePropsResult<{ [key: string]: any }> + >; + }; + ``` + + + + + ### Run the app + + Once done, you can navigate to the `/admin` route. + + You should be able to see the admin dashboard. + diff --git a/apps/docs/pages/v4/docs/glossary.mdx b/apps/docs/pages/v4/docs/glossary.mdx new file mode 100644 index 00000000..8c1f26fe --- /dev/null +++ b/apps/docs/pages/v4/docs/glossary.mdx @@ -0,0 +1,18 @@ +In this documentation we will use the following conventions: + +## List view + +Refers to the view that displays a table of model's records. + +E.g. below is an example of a list view for a `User` model: + +![List view](/docs/list-view.png) + +## Edit view + +Refers to the view that allows you to edit a single record. + +E.g. below is an example of an edit view for a `User` model: + +![Edit view](/docs/edit-view.png) + diff --git a/apps/docs/pages/v4/docs/i18n.mdx b/apps/docs/pages/v4/docs/i18n.mdx new file mode 100644 index 00000000..435fc462 --- /dev/null +++ b/apps/docs/pages/v4/docs/i18n.mdx @@ -0,0 +1,224 @@ +import { Callout } from "nextra/components"; +import OptionsTable from "../../../components/OptionsTable"; + +# I18n + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + +Next Admin supports i18n with the `translations` prop of the `NextAdmin` component. + + +The "Showing from" text in the list indicator, e.g: Showing from 1 to 10 of 25, + defaultValue: "Showing from" + }, + { + name: "list.footer.indicator.to", + type: "string", + description: <>The "to" text in the list indicator, e.g: Showing from 1 to 10 of 25, + defaultValue: "to" + }, + { + name: "list.footer.indicator.of", + type: "string", + description: <>The "of" text in the list indicator, e.g: Showing from 1 to 10 of 25, + defaultValue: "of" + }, + { + name: "list.row.actions.delete.label", + type: "string", + description: "The text in the delete button displayed at the end of each row", + defaultValue: "Delete" + }, + { + name: "list.row.actions.delete.alert", + type: "string", + description: "The text in the native alert when the delete action is called in the list", + defaultValue: "Are you sure you want to delete {{count}} element(s)?" + }, + { + name: "list.row.actions.delete.success", + type: "string", + description: "The text appearing after a successful deletion from the list", + defaultValue: "Deleted successfully" + }, + { + name: "list.row.actions.delete.error", + type: "string", + description: "The text appearing after an error during the deletion from the list", + defaultValue: "An error occured while deleting" + }, + { + name: "list.empty.label", + type: "string", + description: "The text displayed when there is no row in the list", + defaultValue: "No {{resource}} found" + }, + { + name: "list.empty.caption", + type: "string", + description: "The caption displayed when there is no row in the list", + defaultValue: "Get started by creating a new {{resource}}" + }, + { + name: "form.button.save.label", + type: "string", + description: "The text displayed in the form submit button", + defaultValue: "Submit" + }, + { + name: "form.button.delete.label", + type: "string", + description: "The text displayed in the form delete button", + defaultValue: "Delete" + }, + { + name: "form.delete.alert", + type: "string", + description: "The text displayed on the alert when the delete button is clicked", + defaultValue: "Are you sure you want to delete this?" + }, + { + name: "form.widgets.file_upload.label", + type: "string", + description: "The text displayed in file upload widget to select a file", + defaultValue: "Choose a file" + }, + { + name: "form.widgets.file_upload.drag_and_drop", + type: "string", + description: "The text displayed in file upload widget to indicate a drag & drop is possible", + defaultValue: "or drag and drop" + }, + { + name: "form.widgets.file_upload.delete", + type: "string", + description: "The text displayed in file upload widget to delete the current file", + defaultValue: "Delete" + }, + { + name: "form.widgets.multiselect.select", + type: "string", + description: "The text displayed in the multiselect widget in list display mode to toggle the select dialog", + defaultValue: "Select items" + }, + { + name: "actions.label", + type: "string", + description: "The text displayed in the dropdown button for the actions list", + defaultValue: "Action" + }, + { + name: "actions.edit.label", + type: "string", + description: "The text displayed for the default edit action in the actions dropdown", + defaultValue: "Edit" + }, + { + name: "actions.create.label", + type: "string", + description: "The text displayed for the default create action in the actions dropdown", + defaultValue: "Create" + }, + { + name: "actions.delete.label", + type: "string", + description: "The text displayed for the default delete action in the actions dropdown", + defaultValue: "Delete" + }, + { + name: "selector.loading", + type: "string", + description: "The text displayed in the selector widget while loading the options", + defaultValue: "Loading..." + }, + { + name: "user.logout", + type: "string", + description: "The text displayed in the logout button", + defaultValue: "Logout" + }, + { + name: "model", + type: "object", + description: <>Object to custom model and field names. More details, + defaultValue: "{}" + } + ]} +/> + +### Translate model name and fields + +There are two ways to translate these default keys, provide a function named `getMessages` inside the options or provide `translations` props to `NextAdmin` component. + + + Using `getMessages` allows you to provide an object with a multiple level structure to translate the keys, while the `translations` props only allow you to provide a flat object (`form.widgets.file_upload.delete` ex.) + +You can also pass your own set of translations. For example you can set a custom action name as a translation key, which will then be translated by the lib. + +```js +actions: [ + { + title: "actions.user.email", + action: async (...args) => { + "use server"; + const { submitEmail } = await import("./actions/nextadmin"); + await submitEmail(...args); + }, + successMessage: "actions.user.email.success", + errorMessage: "actions.user.email.error", + }, +], +``` + +Here, the `actions.user.email` key will be translated by the lib, and the value will be used as the action title, as well as the success and error messages after the action's execution. + +Currently, you can only translate the following: + +- action title, success and error message +- field validation error message + +Check out the example app for more details on its usage. + +### Translate model name and fields + +By using the `model` key in the translations object, you can translate the model name and fields. + +```json +{ + "model": { + "User": { // Keep the case sensitive name of the model + "name": "User", + "plural": "Users", + "fields": { + "email": "Email", + "password": "Password" + } + } + } +} +``` + + + If you only use one language for your admin, you should prefer to use the `alias` system ([more details](/v4/docs/api/model-configuration)) to customize field names. + \ No newline at end of file diff --git a/apps/docs/pages/v4/docs/index.mdx b/apps/docs/pages/v4/docs/index.mdx new file mode 100644 index 00000000..06844db2 --- /dev/null +++ b/apps/docs/pages/v4/docs/index.mdx @@ -0,0 +1,39 @@ +import { + BoltIcon, + CodeBracketIcon, + PlayCircleIcon, +} from '@heroicons/react/24/outline' +import { Callout, Card, Cards } from 'nextra/components' + +# 🧞 Next Admin + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + + } title="Installation" href="/v4/docs/getting-started" /> + } title="Live demo" href="https://next-admin-po.vercel.app/admin" /> + } title="Source code" href="https://github.com/premieroctet/next-admin" /> + + +###### `next-admin` is a library built on top of [Prisma](https://www.prisma.io/) and [Next.js](https://nextjs.org/) that allows you to easily manage and visualize your Prisma database in a nice graphical user interface (GUI). + +Get started by following the [installation guide](/v4/docs/getting-started) or check out the [live demo](https://next-admin-po.vercel.app/admin). + + +![Hello](/screenshot.png) + +This library is still under development. The following features are available: + +- 👀 Visualize, search and filter your data quickly +- 💅 Customizable admin dashboard +- 💽 Database relationships management +- 👩🏻‍💻 User management (CRUD operations) +- 🎨 Dashboard widgets and customizable panels +- ⚛️ Integration with Prisma ORM +- 👔 Customizable list and form +- ⚙️ Supports App Router and Pages Router + +If you want to request a feature, please open an issue [here](https://github.com/premieroctet/next-admin/issues/new) diff --git a/apps/docs/pages/v4/docs/route.mdx b/apps/docs/pages/v4/docs/route.mdx new file mode 100644 index 00000000..04efd5e9 --- /dev/null +++ b/apps/docs/pages/v4/docs/route.mdx @@ -0,0 +1,63 @@ +import { Callout, FileTree, Tabs } from 'nextra/components' + +# Customize the admin route name + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + + +When setting up `next-admin` you can set the admin panel route name to whatever you want. +The admin panel route name is set by your folder name. + + + +Examples: + + + + You want to have your next admin panel at `https://website.com/admin` + + + + + + + + + You want to have your next admin panel at `https://website.com/prisma` + + + + + + + + + + + You want to have your next admin panel at `https://website.com/admin` + + + + + + + + + + + You want to have your next admin panel at `https://website.com/prisma` + + + + + + + + + + + diff --git a/apps/docs/pages/v4/docs/theming.mdx b/apps/docs/pages/v4/docs/theming.mdx new file mode 100644 index 00000000..f8d3f967 --- /dev/null +++ b/apps/docs/pages/v4/docs/theming.mdx @@ -0,0 +1,61 @@ +import { Callout, FileTree, Tabs } from 'nextra/components' + +# Theming + + This documentation covers an older version of Next Admin. If you are using the + latest version (`>=5.0.0` and above), please refer to the [current + documentation](/docs). + + + +Next Admin comes with a preset that is used to theme the different components of the pages. + +You can add the preset to your Tailwind config presets : + +```js copy filename="tailwind.config.js" +module.exports = { + content: [ + // your own content + "./node_modules/@premieroctet/next-admin/dist/**/*.{js,ts,jsx,tsx}", + ], + presets: [require("@premieroctet/next-admin/dist/preset")], +}; +``` + +## Dark mode support + +The preset sets the `darkMode` option to `class` by default. However, if you wish to adapt to the system's preferences, you can set it to `media` in your own config. + +## Theme override + +You can override the default theme by extending the color palette in your Tailwind config. + +```typescript copy filename="tailwind.config.js" +module.exports = { + content: [ + // your own content + "./node_modules/@premieroctet/next-admin/dist/**/*.{js,ts,jsx,tsx}", + ], + plugins: [], + theme: { + extend: { + colors: { + nextadmin: { + background: { + default: '#FEFEFE' + } + }, + // Dark mode colors + "dark-nextadmin": { + background: { + default: "#2F2F2F" + } + } + } + } + } + presets: [require("@premieroctet/next-admin/dist/preset")], +}; +``` + +Make sure to respect the same structure as the one provided in the preset. diff --git a/apps/docs/public/docs/edit-view.png b/apps/docs/public/docs/edit-view.png new file mode 100644 index 00000000..8f493d34 Binary files /dev/null and b/apps/docs/public/docs/edit-view.png differ diff --git a/apps/docs/public/docs/list-view.png b/apps/docs/public/docs/list-view.png new file mode 100644 index 00000000..54abe8a6 Binary files /dev/null and b/apps/docs/public/docs/list-view.png differ diff --git a/apps/docs/public/docs/notice-exemple.png b/apps/docs/public/docs/notice-exemple.png new file mode 100644 index 00000000..19cf1127 Binary files /dev/null and b/apps/docs/public/docs/notice-exemple.png differ diff --git a/apps/example/actions/nextadmin.ts b/apps/example/actions/nextadmin.ts deleted file mode 100644 index 208055af..00000000 --- a/apps/example/actions/nextadmin.ts +++ /dev/null @@ -1,42 +0,0 @@ -"use server"; - -import { ActionParams, ModelName } from "@premieroctet/next-admin"; -import { - SearchPaginatedResourceParams, - deleteResourceItems, - searchPaginatedResource, - submitForm, -} from "@premieroctet/next-admin/dist/actions"; -import { options } from "../options"; -import { prisma } from "../prisma"; - -const delay = (ms: number) => new Promise((res) => setTimeout(res, ms)); - -export const submitFormAction = async ( - params: ActionParams, - formData: FormData -) => { - return submitForm({ ...params, options, prisma }, formData); -}; - -export const submitEmail = async ( - model: ModelName, - ids: number[] | string[] -) => { - console.log("Sending email to " + ids.length + " users"); - await delay(1000); -}; - -export const deleteItem = async ( - model: ModelName, - ids: string[] | number[] -) => { - return deleteResourceItems(prisma, model, ids); -}; - -export const searchResource = async ( - actionParams: ActionParams, - params: SearchPaginatedResourceParams -) => { - return searchPaginatedResource({ ...actionParams, options, prisma }, params); -}; diff --git a/apps/example/actions/posts.ts b/apps/example/actions/posts.ts deleted file mode 100644 index caab0a6d..00000000 --- a/apps/example/actions/posts.ts +++ /dev/null @@ -1,30 +0,0 @@ -"use server"; - -import { redirect } from "next/navigation"; -import { prisma } from "../prisma"; - -export const createRandomPost = async () => { - const firstUser = await prisma.user.findFirst(); - const post = await prisma.post.create({ - data: { - title: "Random Post", - author: { - connect: { - id: firstUser?.id, - }, - }, - }, - }); - - const params = new URLSearchParams(); - - params.set( - "message", - JSON.stringify({ - type: "success", - content: "Random post created", - }) - ); - - redirect(`/admin/post/${post.id}?${params.toString()}`); -}; diff --git a/apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx b/apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx index 3987db28..ae7237cd 100644 --- a/apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx +++ b/apps/example/app/[locale]/admin/[[...nextadmin]]/page.tsx @@ -1,13 +1,9 @@ -import { - deleteItem, - searchResource, - submitFormAction, -} from "@/actions/nextadmin"; +import Dashboard from "@/components/Dashboard"; import { options } from "@/options"; import { prisma } from "@/prisma"; import schema from "@/prisma/json-schema/json-schema.json"; -import { NextAdmin } from "@premieroctet/next-admin"; -import { getPropsFromParams } from "@premieroctet/next-admin/dist/appRouter"; +import { NextAdmin, PageProps } from "@premieroctet/next-admin"; +import { getNextAdminProps } from "@premieroctet/next-admin/dist/appRouter"; import { Metadata, Viewport } from "next"; import { getMessages } from "next-intl/server"; @@ -23,35 +19,38 @@ export const metadata: Metadata = { export default async function AdminPage({ params, searchParams, -}: { - params: { [key: string]: string[] | string }; - searchParams: { [key: string]: string | string[] | undefined } | undefined; -}) { - const props = await getPropsFromParams({ - params: params.nextadmin as string[], +}: Readonly) { + const props = await getNextAdminProps({ + params: params.nextadmin, searchParams, - options, + basePath: "/admin", + apiBasePath: "/api/admin", prisma, + options, schema, - action: submitFormAction, - deleteAction: deleteItem, - getMessages: () => - getMessages({ locale: params.locale as string }).then( + getMessages: (locale) => + getMessages({ locale }).then( (messages) => messages.admin as Record ), locale: params.locale as string, - searchPaginatedResourceAction: searchResource, }); + const logoutRequest: [RequestInfo, RequestInit] = [ + "/", + { + method: "POST", + }, + ]; + return ( } user={{ data: { name: "John Doe", }, - logoutUrl: "/", + logout: logoutRequest, }} /> ); diff --git a/apps/example/app/[locale]/admin/custom/page.tsx b/apps/example/app/[locale]/admin/custom/page.tsx index 653a7027..9637ef26 100644 --- a/apps/example/app/[locale]/admin/custom/page.tsx +++ b/apps/example/app/[locale]/admin/custom/page.tsx @@ -1,12 +1,13 @@ import { MainLayout } from "@premieroctet/next-admin"; -import { getMainLayoutProps } from "@premieroctet/next-admin/dist/mainLayout"; +import { getMainLayoutProps } from "@premieroctet/next-admin/dist/appRouter"; import { options } from "../../../../options"; import { prisma } from "../../../../prisma"; const CustomPage = async () => { const mainLayoutProps = getMainLayoutProps({ + basePath: "/admin", + apiBasePath: "/api/admin", options, - isAppDir: true, }); const totalUsers = await prisma.user.count(); @@ -26,7 +27,7 @@ const CustomPage = async () => { data: { name: "John Doe", }, - logoutUrl: "/", + logout: ["/"], }} >
diff --git a/apps/example/app/api/admin/[[...nextadmin]]/route.ts b/apps/example/app/api/admin/[[...nextadmin]]/route.ts new file mode 100644 index 00000000..c259aee5 --- /dev/null +++ b/apps/example/app/api/admin/[[...nextadmin]]/route.ts @@ -0,0 +1,13 @@ +import { options } from "@/options"; +import { prisma } from "@/prisma"; +import schema from "@/prisma/json-schema/json-schema.json"; +import { createHandler } from "@premieroctet/next-admin/dist/appHandler"; + +const { run } = createHandler({ + apiBasePath: "/api/admin", + options, + prisma, + schema, +}); + +export { run as DELETE, run as GET, run as POST }; diff --git a/apps/example/components/DatePicker.tsx b/apps/example/components/DatePicker.tsx index 0688366d..010dec76 100644 --- a/apps/example/components/DatePicker.tsx +++ b/apps/example/components/DatePicker.tsx @@ -27,7 +27,7 @@ const DatePicker = ({ value, name, onChange, disabled, required }: Props) => { text-nextadmin-content-inverted dark:text-dark-nextadmin-content-inverted ring-nextadmin-border-default focus:ring-nextadmin-brand-default dark:focus:ring-dark-nextadmin-brand-default block w-full rounded-md border-0 px-2 py-1.5 text-sm shadow-sm ring-1 ring-1 ring-inset ring-inset transition-all duration-300 placeholder:text-gray-400 focus:ring-1 focus:ring-2 focus:ring-inset focus:ring-inset focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 sm:leading-6 [&>div]:border-none" /> - + ); }; diff --git a/apps/example/e2e/004-custom_actions.spec.ts b/apps/example/e2e/004-custom_actions.spec.ts index 4a80f286..1db401b3 100644 --- a/apps/example/e2e/004-custom_actions.spec.ts +++ b/apps/example/e2e/004-custom_actions.spec.ts @@ -14,8 +14,13 @@ test.describe("User's custom actions", () => { await page.getByTestId("actions-dropdown").click(); await expect(page.getByText("Send email")).toBeVisible(); + const response = page.waitForResponse( + (response) => + response.url().includes("/submit-email") && response.status() === 200 + ); await page.getByText("Send email").click(); - await page.waitForURL((url) => !!url.searchParams.get("message")); + await response; + await expect(page.getByText("Email sent successfully")).toBeVisible(); }); @@ -34,12 +39,15 @@ test.describe("User's custom actions", () => { page.getByTestId("actions-dropdown-content").getByText("Delete") ).toBeVisible(); + const response = page.waitForResponse( + (response) => + response.request().method() === "DELETE" && response.status() === 200 + ); await page .getByTestId("actions-dropdown-content") .getByText("Delete") .click(); - - await page.waitForURL((url) => !!url.searchParams.get("message")); + await response; await expect(page.getByText("Deleted successfully")).toBeVisible(); await expect(page.locator("table tbody tr")).toHaveCount(3); }); diff --git a/apps/example/options.tsx b/apps/example/options.tsx index 7797fa3a..bce4a309 100644 --- a/apps/example/options.tsx +++ b/apps/example/options.tsx @@ -2,8 +2,8 @@ import { NextAdminOptions } from "@premieroctet/next-admin"; import DatePicker from "./components/DatePicker"; export const options: NextAdminOptions = { - basePath: "/admin", title: "⚡️ My Admin", + model: { User: { toString: (user) => `${user.name} (${user.email})`, @@ -74,7 +74,7 @@ export const options: NextAdminOptions = { "email-notice": "col-span-4 row-start-3", email: "col-span-4 md:col-span-2 row-start-4", posts: "col-span-4 md:col-span-2 row-start-5", - role: "col-span-4 md:col-span-2 row-start-6", + role: "col-span-4 md:col-span-3 row-start-6", birthDate: "col-span-3 row-start-7", avatar: "col-span-4 row-start-8", metadata: "col-span-4 row-start-9", @@ -104,7 +104,7 @@ export const options: NextAdminOptions = { * Make sure to return a string. */ upload: async (buffer, infos) => { - return "https://www.gravatar.com/avatar/00000000000000000000000000000000"; + return "https://raw.githubusercontent.com/premieroctet/next-admin/33fcd755a34f1ec5ad53ca8e293029528af814ca/apps/example/public/assets/logo.svg"; }, }, }, @@ -126,11 +126,10 @@ export const options: NextAdminOptions = { }, actions: [ { + id: "submit-email", title: "actions.user.email.title", - action: async (...args) => { - "use server"; - const { submitEmail } = await import("./actions/nextadmin"); - await submitEmail(...args); + action: async (ids) => { + console.log("Sending email to " + ids.length + " users"); }, successMessage: "actions.user.email.success", errorMessage: "actions.user.email.error", diff --git a/apps/example/package.json b/apps/example/package.json index 6b79bc61..1428ff1b 100644 --- a/apps/example/package.json +++ b/apps/example/package.json @@ -20,8 +20,8 @@ "dependencies": { "@heroicons/react": "^2.0.18", "@picocss/pico": "^1.5.7", - "@premieroctet/next-admin": "*", - "@prisma/client": "^5.13.0", + "@premieroctet/next-admin": "5.0.0-rc.14", + "@prisma/client": "5.14.0", "@tremor/react": "^3.2.2", "babel-plugin-superjson-next": "^0.4.5", "next": "^14.0.3", diff --git a/apps/example/pageRouterOptions.tsx b/apps/example/pageRouterOptions.tsx index cf988246..74905a11 100644 --- a/apps/example/pageRouterOptions.tsx +++ b/apps/example/pageRouterOptions.tsx @@ -2,7 +2,6 @@ import { NextAdminOptions } from "@premieroctet/next-admin"; import DatePicker from "./components/DatePicker"; export const options: NextAdminOptions = { - basePath: "/pagerouter/admin", title: "⚡️ My Admin Page Router", model: { User: { @@ -77,8 +76,8 @@ export const options: NextAdminOptions = { * for example you can upload the file to an S3 bucket. * Make sure to return a string. */ - upload: async (file, infos) => { - return "https://www.gravatar.com/avatar/00000000000000000000000000000000"; + upload: async (buffer, infos) => { + return "https://raw.githubusercontent.com/premieroctet/next-admin/33fcd755a34f1ec5ad53ca8e293029528af814ca/apps/example/public/assets/logo.svg"; }, }, }, @@ -87,15 +86,9 @@ export const options: NextAdminOptions = { actions: [ { title: "Send email", - action: async (model, ids) => { - const response = await fetch("/api/email", { - method: "POST", - body: JSON.stringify(ids), - }); - - if (!response.ok) { - throw new Error("Failed to send email"); - } + id: "submit-email", + action: async (ids) => { + console.log("Sending email to " + ids.length + " users"); }, successMessage: "Email sent successfully", errorMessage: "Error while sending email", diff --git a/apps/example/pages/api/pagerouter/admin/[[...nextadmin]].ts b/apps/example/pages/api/pagerouter/admin/[[...nextadmin]].ts new file mode 100644 index 00000000..57ba07b7 --- /dev/null +++ b/apps/example/pages/api/pagerouter/admin/[[...nextadmin]].ts @@ -0,0 +1,19 @@ +import { options } from "@/pageRouterOptions"; +import { prisma } from "@/prisma"; +import schema from "@/prisma/json-schema/json-schema.json"; +import { createHandler } from "@premieroctet/next-admin/dist/pageHandler"; + +export const config = { + api: { + bodyParser: false, + }, +}; + +const { run } = createHandler({ + apiBasePath: "/api/pagerouter/admin", + options, + prisma, + schema: schema, +}); + +export default run; diff --git a/apps/example/pages/pagerouter/admin/[[...nextadmin]].tsx b/apps/example/pages/pagerouter/admin/[[...nextadmin]].tsx index 436031ed..e0a76cb8 100644 --- a/apps/example/pages/pagerouter/admin/[[...nextadmin]].tsx +++ b/apps/example/pages/pagerouter/admin/[[...nextadmin]].tsx @@ -1,5 +1,6 @@ import { AdminComponentProps, NextAdmin } from "@premieroctet/next-admin"; -import { GetServerSideProps, GetServerSidePropsResult } from "next"; +import { getNextAdminProps } from "@premieroctet/next-admin/dist/pageRouter"; +import { GetServerSideProps } from "next"; import { options } from "../../../pageRouterOptions"; import { prisma } from "../../../prisma"; import schema from "../../../prisma/json-schema/json-schema.json"; @@ -16,19 +17,18 @@ export default function Admin(props: AdminComponentProps) { data: { name: "John Doe", }, - logoutUrl: "/", + logout: ["/"], }} /> ); } -export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const { nextAdminRouter } = await import( - "@premieroctet/next-admin/dist/router" - ); - - const adminRouter = await nextAdminRouter(prisma, schema, pageOptions); - return adminRouter.run(req, res) as Promise< - GetServerSidePropsResult<{ [key: string]: any }> - >; -}; +export const getServerSideProps: GetServerSideProps = async ({ req }) => + await getNextAdminProps({ + basePath: "/pagerouter/admin", + apiBasePath: "/api/pagerouter/admin", + prisma, + schema, + options: pageOptions, + req, + }); diff --git a/apps/example/pages/pagerouter/admin/custom/index.tsx b/apps/example/pages/pagerouter/admin/custom/index.tsx index 8f7092d1..3550fa61 100644 --- a/apps/example/pages/pagerouter/admin/custom/index.tsx +++ b/apps/example/pages/pagerouter/admin/custom/index.tsx @@ -1,5 +1,5 @@ import { MainLayout, MainLayoutProps } from "@premieroctet/next-admin"; -import { getMainLayoutProps } from "@premieroctet/next-admin/dist/mainLayout"; +import { getMainLayoutProps } from "@premieroctet/next-admin/dist/pageRouter"; import { GetServerSideProps } from "next"; import { options } from "../../../../options"; import { prisma } from "../../../../prisma"; @@ -35,7 +35,7 @@ const CustomPage = ({ data: { name: "John Doe", }, - logoutUrl: "/", + logout: ["/"], }} >
@@ -72,8 +72,9 @@ export const getServerSideProps: GetServerSideProps = async ({ req, }) => { const mainLayoutProps = getMainLayoutProps({ + basePath: "/pagerouter/admin", + apiBasePath: "/api/pagerouter/admin", options: pageOptions, - isAppDir: false, }); if (req.method === "POST") { diff --git a/apps/example/prisma/migrations/20240806101108_category_and_post_set_null_on_delete/migration.sql b/apps/example/prisma/migrations/20240806101108_category_and_post_set_null_on_delete/migration.sql new file mode 100644 index 00000000..68577f6e --- /dev/null +++ b/apps/example/prisma/migrations/20240806101108_category_and_post_set_null_on_delete/migration.sql @@ -0,0 +1,11 @@ +-- DropForeignKey +ALTER TABLE "CategoriesOnPosts" DROP CONSTRAINT "CategoriesOnPosts_categoryId_fkey"; + +-- DropForeignKey +ALTER TABLE "CategoriesOnPosts" DROP CONSTRAINT "CategoriesOnPosts_postId_fkey"; + +-- AddForeignKey +ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/example/prisma/migrations/20240806113632_category_and_post_set_cascade_on_delete/migration.sql b/apps/example/prisma/migrations/20240806113632_category_and_post_set_cascade_on_delete/migration.sql new file mode 100644 index 00000000..30db1371 --- /dev/null +++ b/apps/example/prisma/migrations/20240806113632_category_and_post_set_cascade_on_delete/migration.sql @@ -0,0 +1,11 @@ +-- DropForeignKey +ALTER TABLE "CategoriesOnPosts" DROP CONSTRAINT "CategoriesOnPosts_categoryId_fkey"; + +-- DropForeignKey +ALTER TABLE "CategoriesOnPosts" DROP CONSTRAINT "CategoriesOnPosts_postId_fkey"; + +-- AddForeignKey +ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_postId_fkey" FOREIGN KEY ("postId") REFERENCES "Post"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "CategoriesOnPosts" ADD CONSTRAINT "CategoriesOnPosts_categoryId_fkey" FOREIGN KEY ("categoryId") REFERENCES "Category"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/apps/example/prisma/schema.prisma b/apps/example/prisma/schema.prisma index 86372aeb..6fe459cb 100644 --- a/apps/example/prisma/schema.prisma +++ b/apps/example/prisma/schema.prisma @@ -67,9 +67,9 @@ model Category { model CategoriesOnPosts { id Int @default(autoincrement()) - post Post @relation(fields: [postId], references: [id]) + post Post @relation(fields: [postId], references: [id], onDelete: Cascade) postId Int - category Category @relation(fields: [categoryId], references: [id]) + category Category @relation(fields: [categoryId], references: [id], onDelete: Cascade) categoryId Int order Int @default(0) diff --git a/packages/next-admin/CHANGELOG.md b/packages/next-admin/CHANGELOG.md index ef0ac138..d44fdbec 100644 --- a/packages/next-admin/CHANGELOG.md +++ b/packages/next-admin/CHANGELOG.md @@ -1,10 +1,82 @@ # @premieroctet/next-admin +## 5.0.0-rc.14 + +### Patch Changes + +- add dist + +## 5.0.0-rc.13 + +### Patch Changes + +- add URL redirect support for logout + +## 5.0.0-rc.12 + +### Patch Changes + +- [170a48b](https://github.com/premieroctet/next-admin/commit/170a48b): Fix images CORS issues + +## 5.0.0-rc.11 + +### Patch Changes + +- Fix date input and add time-second format + +## 5.0.0-rc.10 + +### Patch Changes + +- Add `isDirty` for form to submit only fields touched + +## 5.0.0-rc.9 + +### Patch Changes + +- Change logout system (Request or server action) + +## 5.0.0-rc.8 + +### Patch Changes + +- Small fixes (select, dark mode, dashboard, layout, doc) + +## 5.0.0-rc.7 + +### Patch Changes + +- Redirect useEffect + +## 5.0.0-rc.6 + +### Patch Changes + +- [60afe2f](https://github.com/premieroctet/next-admin/commit/60afe2f): Add history on redirect `Save` + +## 5.0.0-rc.5 + +### Patch Changes + +- Dependency `next-themes` + +## 5.0.0-rc.4 + +### Patch Changes + +- [f120d10](https://github.com/premieroctet/next-admin/commit/f120d10): Add `next-themes` to handle color scheme + +## 5.0.0-rc.3 + +### Patch Changes + +- Merge main branch + ## 4.4.5 ### Patch Changes -- [e52ed18](https://github.com/premieroctet/next-admin/commit/e52ed18): Don't prefetch export Link in header +- - [e52ed18](https://github.com/premieroctet/next-admin/commit/e52ed18): Don't prefetch export Link in header - [8512b5e](https://github.com/premieroctet/next-admin/commit/8512b5e): Restore Buffer for `upload` function, add informations as second parameter - [6da22fc](https://github.com/premieroctet/next-admin/commit/6da22fc): Add loader on select diff --git a/packages/next-admin/README.md b/packages/next-admin/README.md index ede50da1..7fe3376e 100644 --- a/packages/next-admin/README.md +++ b/packages/next-admin/README.md @@ -27,7 +27,7 @@ To install the library, run the following command: -```shell +```shell copy yarn add @premieroctet/next-admin prisma-json-schema-generator ``` @@ -39,126 +39,92 @@ For detailed documentation, please refer to the [documentation](https://next-adm To use the library in your Next.js application, follow these steps: -1. Create an admin route in your Next.js application. -2. Add the file `[[...nextadmin]].js` to the `pages/admin` directory. -3. Export the `NextAdmin` component from the file. -4. Use `getServerSideProps` to pass the `props` from the `nextAdminRouter` to the `NextAdmin` component. - -Bonus: Customize the admin dashboard by passing the `NextAdminOptions` options to the router and customize the admin dashboard by passing `dashboard` props to `NextAdmin` component. (More details in the [documentation](https://next-admin-docs.vercel.app/)) - -## Example - -Here's a basic example of how to use the library: - -#### App router - -Set Next Admin server actions into your app: - -```ts -// actions/nextadmin.ts -"use server"; -import { ActionParams, ModelName } from "@premieroctet/next-admin"; -import { - deleteResourceItems, - submitForm, - searchPaginatedResource, - SearchPaginatedResourceParams, -} from "@premieroctet/next-admin/dist/actions"; -import { prisma } from "../prisma"; -import { options } from "../options"; - -export const submitFormAction = async ( - params: ActionParams, - formData: FormData -) => { - return submitForm({ ...params, options, prisma }, formData); -}; - -export const deleteItem = async ( - model: ModelName, - ids: string[] | number[] -) => { - return deleteResourceItems(prisma, model, ids); -}; - -export const searchResource = async ( - actionParams: ActionParams, - params: SearchPaginatedResourceParams -) => { - return searchPaginatedResource({ ...actionParams, options, prisma }, params); +1. Add tailwind preset to your `tailwind.config.js` file - [more details](http://next-admin-docs.vercel.app/docs/getting-started#tailwindcss) +2. Add json schema generator to your Prisma schema file - [more details](http://next-admin-docs.vercel.app/docs/getting-started#prisma) +3. Generate the schema with `yarn run prisma generate` +4. Create a catch-all segment page `page.tsx` in the `app/admin/[[...nextadmin]]` folder - [more details](http://next-admin-docs.vercel.app/docs/getting-started#page-nextadmin) +5. Create an catch-all API route `route.ts` in the `app/api/[[...nextadmin]]` folder - [more details](http://next-admin-docs.vercel.app/docs/getting-started#api-route-nextadmin) + +Bonus: Customize the admin dashboard by passing the `NextAdminOptions` options to the router and customize the admin dashboard by passing `dashboard` props to `NextAdmin` component. (More details in the [documentation](http://next-admin-docs.vercel.app/docs/getting-started#next-admin-options---optional)) + +## What does it look like? + +An example of `next-admin` options: + +```tsx +// app/admin/options.ts +import { NextAdminOptions } from "@premieroctet/next-admin"; + +export const options: NextAdminOptions = { + title: "⚡️ My Admin Page", + model: { + User: { + toString: (user) => `${user.name} (${user.email})`, + title: "Users", + icon: "UsersIcon", + list: { + search: ["name", "email"], + filters: [ + { + name: "is Admin", + active: false, + value: { + role: { + equals: "ADMIN", + }, + }, + }, + ], + }, + }, + Post: { + toString: (post) => `${post.title}`, + }, + Category: { + title: "Categories", + icon: "InboxStackIcon", + toString: (category) => `${category.name}`, + list: { + display: ["name", "posts"], + }, + edit: { + display: ["name", "posts"], + }, + }, + }, + pages: { + "/custom": { + title: "Custom page", + icon: "AdjustmentsHorizontalIcon", + }, + }, + externalLinks: [ + { + label: "Website", + url: "https://www.myblog.com", + }, + ], + sidebar: { + groups: [ + { + title: "Users", + models: ["User"], + }, + { + title: "Categories", + models: ["Category"], + }, + ], + }, }; ``` -Then configure `page.tsx` - -```tsx -// app/admin/[[...nextadmin]]/page.tsx -import { NextAdmin } from "@premieroctet/next-admin"; -import { getPropsFromParams } from "@premieroctet/next-admin/dist/appRouter"; -import "@premieroctet/next-admin/dist/styles.css"; -import Dashboard from "../../../components/Dashboard"; -import { options } from "../../../options"; -import { prisma } from "../../../prisma"; -import schema from "../../../prisma/json-schema/json-schema.json"; // generated by prisma-json-schema-generator on yarn run prisma generate -import { - submitFormAction, - deleteItem, - submitFormAction, -} from "../../../actions/nextadmin"; - -export default async function AdminPage({ - params, - searchParams, -}: { - params: { [key: string]: string[] }; - searchParams: { [key: string]: string | string[] | undefined } | undefined; -}) { - const props = await getPropsFromParams({ - params: params.nextadmin, - searchParams, - options, - prisma, - schema, - action: submitFormAction, - deleteAction: deleteItem, - searchPaginatedResourceAction: searchResource, - }); - - return ; -} -``` -#### Page Router - -```tsx -import { PrismaClient } from "@prisma/client"; -import schema from "./../../../prisma/json-schema/json-schema.json"; // generated by prisma-json-schema-generator on yarn run prisma generate -import "@premieroctet/next-admin/dist/styles.css"; -import { - AdminComponentProps, - NextAdmin, - NextAdminOptions, -} from "@premieroctet/next-admin"; - -const prisma = new PrismaClient(); - -export default function Admin(props: AdminComponentProps) { - return ; -} - -export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { - const { nextAdminRouter } = await import( - "@premieroctet/next-admin/dist/router" - ); - - const adminRouter = await nextAdminRouter(prisma, schema); - return adminRouter.run(req, res) as Promise< - GetServerSidePropsResult<{ [key: string]: any }> - >; -}; -``` +## 📄 Documentation + +For detailed documentation, please refer to the [documentation](https://next-admin-docs.vercel.app/). -## Demonstration +## 🚀 Demonstration You can find the library code in the [next-admin](https://github.com/premieroctet/next-admin) repository. diff --git a/packages/next-admin/package.json b/packages/next-admin/package.json index 293c4afb..84837093 100644 --- a/packages/next-admin/package.json +++ b/packages/next-admin/package.json @@ -1,6 +1,6 @@ { "name": "@premieroctet/next-admin", - "version": "4.4.5", + "version": "5.0.0-rc.14", "main": "./dist/index.js", "description": "Next-Admin provides a customizable and turnkey admin dashboard for applications built with Next.js and powered by the Prisma ORM. It aims to simplify the development process by providing a turnkey admin system that can be easily integrated into your project.", "keywords": [ @@ -40,8 +40,12 @@ }, "peerDependencies": { "@prisma/client": ">=4", + "next": ">=12", "prisma": ">=4", - "prisma-json-schema-generator": ">=3 <=5.1.1" + "prisma-json-schema-generator": ">=3 <=5.1.1", + "react": ">=17", + "react-dom": ">=17", + "typescript": ">=4" }, "dependencies": { "@dnd-kit/core": "^6.1.0", @@ -55,9 +59,9 @@ "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-switch": "^1.0.3", "@radix-ui/react-tooltip": "^1.0.7", - "@rjsf/core": "^5.3.0", - "@rjsf/utils": "^5.3.0", - "@rjsf/validator-ajv8": "^5.3.0", + "@rjsf/core": "^5.19.3", + "@rjsf/utils": "^5.19.3", + "@rjsf/validator-ajv8": "^5.19.3", "@tanstack/react-table": "^8.9.2", "@types/formidable": "^3.4.5", "@types/node": "^18.15.3", @@ -66,13 +70,10 @@ "clsx": "^1.2.1", "formidable": "^3.5.1", "lodash.range": "^3.2.0", - "next": "13.2.4", "next-connect": "^1.0.0-next.3", + "next-themes": "^0.3.0", "nextjs-toploader": "^1.6.6", - "react": "18.2.0", - "react-dom": "18.2.0", "tailwind-merge": "^2.3.0", - "typescript": "^5.1.6", "util": "^0.12.5" }, "devDependencies": { diff --git a/packages/next-admin/src/actions/index.ts b/packages/next-admin/src/actions/index.ts deleted file mode 100644 index e611836c..00000000 --- a/packages/next-admin/src/actions/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./form"; -export * from "./resources"; diff --git a/packages/next-admin/src/actions/resources.ts b/packages/next-admin/src/actions/resources.ts deleted file mode 100644 index a33124b3..00000000 --- a/packages/next-admin/src/actions/resources.ts +++ /dev/null @@ -1,59 +0,0 @@ -"use server"; - -import { PrismaClient } from "@prisma/client"; -import { ActionFullParams, ModelName } from "../types"; -import { optionsFromResource } from "../utils/prisma"; -import { getModelIdProperty } from "../utils/server"; -import { uncapitalize } from "../utils/tools"; - -export const deleteResourceItems = async ( - prisma: PrismaClient, - model: M, - ids: string[] | number[] -) => { - const modelIdProperty = getModelIdProperty(model); - // @ts-expect-error - await prisma[uncapitalize(model)].deleteMany({ - where: { - [modelIdProperty]: { in: ids }, - }, - }); -}; - -export type SearchPaginatedResourceParams = { - originModel: string; - property: string; - model: string; - query: string; - page?: number; - perPage?: number; -}; - -export const searchPaginatedResource = async ( - { options, prisma }: ActionFullParams, - { - originModel, - property, - model, - query, - page = 1, - perPage = 25, - }: SearchPaginatedResourceParams -) => { - const data = await optionsFromResource({ - prisma, - originResource: originModel as ModelName, - property: property, - resource: model as ModelName, - options, - context: {}, - searchParams: new URLSearchParams({ - search: query, - page: page.toString(), - itemsPerPage: perPage.toString(), - }), - appDir: true, - }); - - return data; -}; diff --git a/packages/next-admin/src/appHandler.ts b/packages/next-admin/src/appHandler.ts new file mode 100644 index 00000000..c379a404 --- /dev/null +++ b/packages/next-admin/src/appHandler.ts @@ -0,0 +1,188 @@ +import { createEdgeRouter } from "next-connect"; +import { NextRequest, NextResponse } from "next/server"; +import { handleOptionsSearch } from "./handlers/options"; +import { deleteResource, submitResource } from "./handlers/resources"; +import { CreateAppHandlerParams, Permission, RequestContext } from "./types"; +import { hasPermission } from "./utils/permissions"; +import { + formatId, + getFormValuesFromFormData, + getResourceFromParams, + getResources, +} from "./utils/server"; + +export const createHandler =

({ + apiBasePath, + options, + prisma, + paramKey = "nextadmin" as P, + onRequest, + schema, +}: CreateAppHandlerParams

) => { + const router = createEdgeRouter>(); + const resources = getResources(options); + + if (onRequest) { + router.use(async (req, ctx, next) => { + const response = await onRequest(req, ctx); + + if (response) { + return response; + } + + return next(); + }); + } + + router + .post(`${apiBasePath}/:model/actions/:id`, async (req, ctx) => { + const id = ctx.params[paramKey].at(-1)!; + + // Make sure we don't have a false positive with a model that could be named actions + const resource = getResourceFromParams( + [ctx.params[paramKey][0]], + resources + ); + + if (!resource) { + return NextResponse.json( + { error: "Resource not found" }, + { status: 404 } + ); + } + + const modelAction = options?.model?.[resource]?.actions?.find( + (action) => action.id === id + ); + + if (!modelAction) { + return NextResponse.json( + { error: "Action not found" }, + { status: 404 } + ); + } + + const body = await req.json(); + + try { + await modelAction.action(body as string[] | number[]); + + return NextResponse.json({ ok: true }); + } catch (e) { + return NextResponse.json( + { error: (e as Error).message }, + { status: 500 } + ); + } + }) + .post(`${apiBasePath}/options`, async (req, ctx) => { + const body = await req.json(); + const data = await handleOptionsSearch(body, prisma, options); + + return NextResponse.json(data); + }) + .post(`${apiBasePath}/:model/:id?`, async (req, ctx) => { + const resource = getResourceFromParams(ctx.params[paramKey], resources); + + if (!resource) { + return NextResponse.json( + { error: "Resource not found" }, + { status: 404 } + ); + } + + const body = await getFormValuesFromFormData(await req.formData()); + const id = + ctx.params[paramKey].length === 2 + ? formatId(resource, ctx.params[paramKey].at(-1)!) + : undefined; + + try { + const response = await submitResource({ + prisma, + resource, + body, + id, + options, + schema, + }); + + if (response.error) { + return NextResponse.json( + { error: response.error, validation: response.validation }, + { status: 400 } + ); + } + + return NextResponse.json(response, { status: id ? 200 : 201 }); + } catch (e) { + return NextResponse.json( + { error: (e as Error).message }, + { status: 500 } + ); + } + }) + .delete(`${apiBasePath}/:model/:id`, async (req, ctx) => { + const resource = getResourceFromParams(ctx.params[paramKey], resources); + + if (!resource) { + return NextResponse.json( + { error: "Resource not found" }, + { status: 404 } + ); + } + + if (!hasPermission(options?.model?.[resource], Permission.DELETE)) { + return NextResponse.json( + { error: "You don't have permission to delete this resource" }, + { status: 403 } + ); + } + + await deleteResource({ + body: [ctx.params[paramKey][1]], + prisma, + resource, + }); + + return NextResponse.json({ ok: true }); + }) + .delete(`${apiBasePath}/:model`, async (req, ctx) => { + const resource = getResourceFromParams(ctx.params[paramKey], resources); + + if (!resource) { + return NextResponse.json( + { error: "Resource not found" }, + { status: 404 } + ); + } + + if (!hasPermission(options?.model?.[resource], Permission.DELETE)) { + return NextResponse.json( + { error: "You don't have permission to delete this resource" }, + { status: 403 } + ); + } + try { + const body = await req.json(); + + await deleteResource({ body, prisma, resource }); + + return NextResponse.json({ ok: true }); + } catch (e) { + return NextResponse.json( + { error: (e as Error).message }, + { status: 500 } + ); + } + }); + + const executeRouteHandler = ( + req: NextRequest, + context: RequestContext

+ ) => { + return router.run(req, context) as Promise; + }; + + return { run: executeRouteHandler, router }; +}; diff --git a/packages/next-admin/src/appRouter.ts b/packages/next-admin/src/appRouter.ts index ad246d7f..d06a04ec 100644 --- a/packages/next-admin/src/appRouter.ts +++ b/packages/next-admin/src/appRouter.ts @@ -1,9 +1,9 @@ -"use server"; -import { - GetPropsFromParamsParams, - getPropsFromParams as _getPropsFromParams, -} from "./utils/props"; +import { GetMainLayoutPropsParams, GetNextAdminPropsParams } from "./types"; +import { getMainLayoutProps as _getMainLayoutProps, getPropsFromParams as _getPropsFromParams } from "./utils/props"; -export const getPropsFromParams = ( - params: Omit -) => _getPropsFromParams({ ...params, isAppDir: true }); +export const getNextAdminProps = async (params: Omit) => { + "use server"; + return _getPropsFromParams({ ...params, isAppDir: true }); +}; + +export const getMainLayoutProps = (args: Omit) => _getMainLayoutProps({ ...args, isAppDir: true }); diff --git a/packages/next-admin/src/components/ActionsDropdown.tsx b/packages/next-admin/src/components/ActionsDropdown.tsx index 2e450f24..fb7a51e8 100644 --- a/packages/next-admin/src/components/ActionsDropdown.tsx +++ b/packages/next-admin/src/components/ActionsDropdown.tsx @@ -17,7 +17,7 @@ import { } from "./radix/Dropdown"; type Props = { - actions: ModelAction[]; + actions: Array>; selectedIds: string[] | number[]; resource: ModelName; selectedCount?: number; @@ -33,7 +33,7 @@ const ActionsDropdown = ({ const { t } = useI18n(); const [isOpen, setIsOpen] = useState(false); - const onActionClick = (action: ModelAction) => { + const onActionClick = (action: ModelAction | Omit) => { runAction(action); }; @@ -41,7 +41,7 @@ const ActionsDropdown = ({