From ac29d004dc51cea2f14d269dfab40075c58096fc Mon Sep 17 00:00:00 2001 From: Colin Regourd Date: Fri, 1 Sep 2023 11:48:16 +0200 Subject: [PATCH 001/273] add MIT license --- LICENSE.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 00000000..ab2d59d2 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) [2023] [Premier Octet - Next Admin] + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file From 4b3257a3711f060572828dd2ca8a31c378313a70 Mon Sep 17 00:00:00 2001 From: Colin Regourd Date: Fri, 1 Sep 2023 11:50:19 +0200 Subject: [PATCH 002/273] add license --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index ab2d59d2..f69dff2e 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ MIT License -Copyright (c) [2023] [Premier Octet - Next Admin] +Copyright (c) 2023, Premier Octet - Next Admin Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 460f9e09337d935e8f39ac24eb1c0ab3f5f97c51 Mon Sep 17 00:00:00 2001 From: Colin Regourd Date: Fri, 1 Sep 2023 11:52:16 +0200 Subject: [PATCH 003/273] rename license file --- LICENSE.md => LICENSE | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename LICENSE.md => LICENSE (100%) diff --git a/LICENSE.md b/LICENSE similarity index 100% rename from LICENSE.md rename to LICENSE From 6b0315dc61ea23bbb9351d12b59ac63b0f4f50d8 Mon Sep 17 00:00:00 2001 From: Thibault Date: Mon, 4 Sep 2023 15:56:54 +0200 Subject: [PATCH 004/273] Fix documentation link --- apps/docs/pages/index.mdx | 2 +- apps/docs/theme.config.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx index b65bbbe4..accecf08 100644 --- a/apps/docs/pages/index.mdx +++ b/apps/docs/pages/index.mdx @@ -2,7 +2,7 @@ ###### `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. -Get started by following the [installation guide](/docs/getting-started) or check out the [live demo](https://next-admin-po.vercel.app/admin). +Get started by following the [installation guide](/docs/getting-started) or check out the [live demo](https://next-admin-po.vercel.app/admindemo). ![Hello](/screenshot.png) diff --git a/apps/docs/theme.config.jsx b/apps/docs/theme.config.jsx index 863e3c00..4cb71ac7 100644 --- a/apps/docs/theme.config.jsx +++ b/apps/docs/theme.config.jsx @@ -9,7 +9,7 @@ const config = { }, docsRepositoryBase: "https://github.com/premieroctet/next-admin", footer: { - text: "MIT 2020 © Premier Octet.", + text: `MIT ${new Date().getFullYear()} © Premier Octet.`, }, darkMode: true, primaryHue: 290, From ae8cf354d1d57cef1a8d30d9995e8f1b6d8fd9df Mon Sep 17 00:00:00 2001 From: Colin Regourd Date: Thu, 31 Aug 2023 19:14:06 +0200 Subject: [PATCH 005/273] change type file --- .../pages/admindemo/[[...nextadmin]].tsx | 2 + packages/next-admin/src/router.tsx | 13 +- packages/next-admin/src/types.ts | 125 +++++++++--------- packages/next-admin/src/utils/server.ts | 5 +- packages/next-admin/src/utils/validator.ts | 10 +- 5 files changed, 85 insertions(+), 70 deletions(-) diff --git a/apps/example/pages/admindemo/[[...nextadmin]].tsx b/apps/example/pages/admindemo/[[...nextadmin]].tsx index 153a1e4f..6959be16 100644 --- a/apps/example/pages/admindemo/[[...nextadmin]].tsx +++ b/apps/example/pages/admindemo/[[...nextadmin]].tsx @@ -55,6 +55,8 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { posts: { search: true, display: true, + + }, }, }, diff --git a/packages/next-admin/src/router.tsx b/packages/next-admin/src/router.tsx index 244b140b..67c8678b 100644 --- a/packages/next-admin/src/router.tsx +++ b/packages/next-admin/src/router.tsx @@ -208,8 +208,14 @@ export const nextAdminRouter = async ( ); schema = removeHiddenProperties(schema, edit, resource); await getBody(req, res); + + + console.log("req", req) + + // @ts-expect-error const { id, ...formData } = req.body as Body>; + const dmmfSchema = getPrismaModelForResource(resource); try { // Delete redirect, display the list (this is needed because next keeps the HTTP method on redirects) @@ -260,8 +266,13 @@ export const nextAdminRouter = async ( // Update let data; + const fields = options.model?.[resource]?.edit?.fields as EditFieldsOptions; + + console.log("fields", fields) + console.log("formData", formData) + // Validate - validate(formData, options.model?.[resource]?.edit?.fields) + validate(formData, fields) if (resourceId !== undefined) { // @ts-expect-error diff --git a/packages/next-admin/src/types.ts b/packages/next-admin/src/types.ts index 723f9e91..fccc2705 100644 --- a/packages/next-admin/src/types.ts +++ b/packages/next-admin/src/types.ts @@ -1,33 +1,26 @@ -import { Prisma, PrismaClient } from "@prisma/client"; +import { Prisma } from "@prisma/client"; import { JSONSchema7 } from "json-schema"; import { ReactNode } from "react"; import { PropertyValidationError } from "./exceptions/ValidationError"; -let prisma: PrismaClient; - -if (typeof window === "undefined") { - prisma = new PrismaClient(); -} +/** Type for Model */ export type ModelName = Prisma.ModelName; -export type Field

= - | keyof (typeof Prisma)[`${Capitalize

}ScalarFieldEnum`] - | keyof Prisma.TypeMap["model"][P]["payload"]["objects"]; +export type ScalarField = Prisma.TypeMap["model"][T]["payload"]["scalars"]; +export type ObjectField = Prisma.TypeMap["model"][T]["payload"]["objects"]; -export type UField = Field; +export type Model = ScalarField & { + [P in keyof ObjectField]: + ObjectField[P] extends { scalars: infer S } ? S + : ObjectField[P] extends { scalars: infer S } | null ? S | null + : ObjectField[P] extends { scalars: infer S }[] ? S[] + : never; +} -export type ModelOptions = { - [P in T]?: { - toString?: (item: Model

) => string; - list?: { - fields: ListFieldsOptions

; - }; - edit?: { - fields: EditFieldsOptions

; - }; - }; -}; +export type Field

= keyof Model

; + +/** Type for Options */ export type ListFieldsOptions = { [P in Field]?: { @@ -39,8 +32,19 @@ export type ListFieldsOptions = { export type EditFieldsOptions = { [P in Field]?: { display?: boolean; - // TODO Improve typing - validate?: (value: any) => true | string; + validate?: (value: Model[P]) => true | string; + }; +}; + +export type ModelOptions = { + [P in T]?: { + toString?: (item: Model

) => string; + list?: { + fields: ListFieldsOptions

; + }; + edit?: { + fields: EditFieldsOptions

; + }; }; }; @@ -49,6 +53,9 @@ export type NextAdminOptions = { model?: ModelOptions; }; + +/** Type for Schema */ + export type SchemaProperty = { [P in Field]?: JSONSchema7; }; @@ -67,6 +74,8 @@ export type Schema = Partial> & { definitions: SchemaDefinitions; }; + + export type FormData = { [P in Field]?: string; }; @@ -104,12 +113,6 @@ export type PrismaListRequest = { take?: number; }; -export type Collection = Awaited< - ReturnType<(typeof prisma)[Uncapitalize]["findMany"]> ->; - -export type Model = Collection[number]; - export type ListData = ListDataItem[]; export type ListDataItem = Model & @@ -133,40 +136,40 @@ export type ListDataFieldValue = value: Date; }; - export type ListComponentFieldsOptions = { - [P in Field]?: { - formatter?: (item: ListDataItem) => ReactNode; - }; +export type ListComponentFieldsOptions = { + [P in Field]?: { + formatter?: (item: ListDataItem) => ReactNode; }; - - export type AdminComponentOptions = { - model?: { - [P in T]?: { - toString?: (item: Model

) => string; - list?: { - fields: ListComponentFieldsOptions

; - }; +}; + +export type AdminComponentOptions = { + model?: { + [P in T]?: { + toString?: (item: Model

) => string; + list?: { + fields: ListComponentFieldsOptions

; }; }; }; - - export type AdminComponentProps = { - basePath: string; - schema: Schema; - data?: ListData; - resource: ModelName; - message?: { - type: "success" | "info"; - content: string; - }; - error?: string; - validation?: PropertyValidationError[]; - resources?: ModelName[]; - total?: number; - dmmfSchema: Prisma.DMMF.Field[]; - options?: AdminComponentOptions; +}; + +export type AdminComponentProps = { + basePath: string; + schema: Schema; + data?: ListData; + resource: ModelName; + message?: { + type: "success" | "info"; + content: string; }; - - export type CustomUIProps = { - dashboard?: JSX.Element | (() => JSX.Element); - }; \ No newline at end of file + error?: string; + validation?: PropertyValidationError[]; + resources?: ModelName[]; + total?: number; + dmmfSchema: Prisma.DMMF.Field[]; + options?: AdminComponentOptions; +}; + +export type CustomUIProps = { + dashboard?: JSX.Element | (() => JSX.Element); +}; \ No newline at end of file diff --git a/packages/next-admin/src/utils/server.ts b/packages/next-admin/src/utils/server.ts index a64a7603..297abf00 100644 --- a/packages/next-admin/src/utils/server.ts +++ b/packages/next-admin/src/utils/server.ts @@ -9,7 +9,6 @@ import { FormData, Enumeration, ListFieldsOptions, - UField, EditFieldsOptions, } from "../types"; import { createWherePredicate } from "./prisma"; @@ -353,8 +352,8 @@ export const removeHiddenProperties = ( if (!editOptions) return schema; const properties = schema.definitions[resource].properties; Object.keys(properties).forEach((property) => { - if (!editOptions[property as UField]?.display) { - delete properties[property as UField]; + if (!editOptions[property as Field]?.display) { + delete properties[property as Field]; } }); return schema; diff --git a/packages/next-admin/src/utils/validator.ts b/packages/next-admin/src/utils/validator.ts index 4e005e11..ff1c76a9 100644 --- a/packages/next-admin/src/utils/validator.ts +++ b/packages/next-admin/src/utils/validator.ts @@ -1,14 +1,13 @@ -import { Prisma } from "@prisma/client"; -import { EditFieldsOptions } from "../types"; +import { EditFieldsOptions, ModelName } from "../types"; import { PropertyValidationError, ValidationError, } from "../exceptions/ValidationError"; -export function validate( +export const validate = ( formData: { [key: string]: string }, - fieldsOptions?: EditFieldsOptions -) { + fieldsOptions?: EditFieldsOptions +) => { if (!fieldsOptions) { return; } @@ -18,6 +17,7 @@ export function validate( for (property in fieldsOptions) { if (fieldsOptions[property]?.validate) { const validation = fieldsOptions[property]!.validate!( + // @ts-ignore formData[property as string] ); From e8fd3643c1eeaac76e38df72698b8186ef044c77 Mon Sep 17 00:00:00 2001 From: Colin Regourd Date: Fri, 1 Sep 2023 17:37:57 +0200 Subject: [PATCH 006/273] remove console.log --- packages/next-admin/src/router.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/packages/next-admin/src/router.tsx b/packages/next-admin/src/router.tsx index 67c8678b..c6167815 100644 --- a/packages/next-admin/src/router.tsx +++ b/packages/next-admin/src/router.tsx @@ -209,10 +209,6 @@ export const nextAdminRouter = async ( schema = removeHiddenProperties(schema, edit, resource); await getBody(req, res); - - console.log("req", req) - - // @ts-expect-error const { id, ...formData } = req.body as Body>; @@ -268,9 +264,6 @@ export const nextAdminRouter = async ( const fields = options.model?.[resource]?.edit?.fields as EditFieldsOptions; - console.log("fields", fields) - console.log("formData", formData) - // Validate validate(formData, fields) From 0f05b286acf3e7b1c012bdddc49ca402e7693cfd Mon Sep 17 00:00:00 2001 From: Colin Regourd Date: Fri, 1 Sep 2023 19:06:12 +0200 Subject: [PATCH 007/273] add parsed data function --- packages/next-admin/src/router.tsx | 6 ++- packages/next-admin/src/types.ts | 16 +++++--- packages/next-admin/src/utils/server.ts | 46 +++++++++++++++++++--- packages/next-admin/src/utils/validator.ts | 6 +-- 4 files changed, 58 insertions(+), 16 deletions(-) diff --git a/packages/next-admin/src/router.tsx b/packages/next-admin/src/router.tsx index c6167815..6ffb3403 100644 --- a/packages/next-admin/src/router.tsx +++ b/packages/next-admin/src/router.tsx @@ -17,6 +17,7 @@ import { getResourceIdFromUrl, removeHiddenProperties, getResources, + parseFormData, } from "./utils/server"; import { NextAdminOptions, @@ -213,6 +214,9 @@ export const nextAdminRouter = async ( const { id, ...formData } = req.body as Body>; const dmmfSchema = getPrismaModelForResource(resource); + + const parsedFormData = parseFormData(formData, dmmfSchema?.fields!); + try { // Delete redirect, display the list (this is needed because next keeps the HTTP method on redirects) if (resourceId === undefined && formData.action === "delete") { @@ -265,7 +269,7 @@ export const nextAdminRouter = async ( const fields = options.model?.[resource]?.edit?.fields as EditFieldsOptions; // Validate - validate(formData, fields) + validate(parsedFormData, fields) if (resourceId !== undefined) { // @ts-expect-error diff --git a/packages/next-admin/src/types.ts b/packages/next-admin/src/types.ts index fccc2705..5b6938b9 100644 --- a/packages/next-admin/src/types.ts +++ b/packages/next-admin/src/types.ts @@ -10,16 +10,20 @@ export type ModelName = Prisma.ModelName; export type ScalarField = Prisma.TypeMap["model"][T]["payload"]["scalars"]; export type ObjectField = Prisma.TypeMap["model"][T]["payload"]["objects"]; -export type Model = ScalarField & { - [P in keyof ObjectField]: - ObjectField[P] extends { scalars: infer S } ? S - : ObjectField[P] extends { scalars: infer S } | null ? S | null - : ObjectField[P] extends { scalars: infer S }[] ? S[] +export type Model = ScalarField & { + [P in keyof ObjectField]: + ObjectField[P] extends { scalars: infer S } ? T extends never ? S : T + : ObjectField[P] extends { scalars: infer S } | null ? T extends never ? S | null : T | null + : ObjectField[P] extends { scalars: infer S }[] ? T extends never ? S[] : T[] : never; } +export type ModelWithoutRelationships = Model; + export type Field

= keyof Model

; +/** Type for Form */ + /** Type for Options */ export type ListFieldsOptions = { @@ -32,7 +36,7 @@ export type ListFieldsOptions = { export type EditFieldsOptions = { [P in Field]?: { display?: boolean; - validate?: (value: Model[P]) => true | string; + validate?: (value: ModelWithoutRelationships[P]) => true | string; }; }; diff --git a/packages/next-admin/src/utils/server.ts b/packages/next-admin/src/utils/server.ts index 297abf00..2b82bca1 100644 --- a/packages/next-admin/src/utils/server.ts +++ b/packages/next-admin/src/utils/server.ts @@ -10,6 +10,8 @@ import { Enumeration, ListFieldsOptions, EditFieldsOptions, + ModelWithoutRelationships, + ScalarField, } from "../types"; import { createWherePredicate } from "./prisma"; import { isNativeFunction, uncapitalize } from "./tools"; @@ -61,7 +63,7 @@ export const fillRelationInSchema = async ( if (fieldKind === "enum") { const fieldValue = schema.definitions[modelName].properties[ - field.name as Field + field.name as Field ]; if (fieldValue) { fieldValue.enum = fieldValue.enum?.map((item) => @@ -126,7 +128,7 @@ export const fillRelationInSchema = async ( } else { const fieldValue = schema.definitions[modelName].properties[ - field.name as Field + field.name as Field ]; if (fieldValue) { let enumeration: Enumeration[] = []; @@ -220,9 +222,8 @@ export const findRelationInData = async ( type: "link", value: { label: item[relationProperty], - url: `${dmmfProperty.type as ModelName}/${ - item[relationProperty] - }`, + url: `${dmmfProperty.type as ModelName}/${item[relationProperty] + }`, }, }; } else { @@ -257,6 +258,39 @@ export const findRelationInData = async ( return data; }; + +export const parseFormData = ( + formData: FormData, + dmmfSchema: Prisma.DMMF.Field[] +): Partial> => { + const parsedData: Partial> = {}; + dmmfSchema.forEach((dmmfProperty) => { + if (dmmfProperty.name in formData) { + const dmmfPropertyName = dmmfProperty.name as keyof ScalarField; + const dmmfPropertyType = dmmfProperty.type; + const dmmfPropertyKind = dmmfProperty.kind; + if (dmmfPropertyKind === "object") { + if(Boolean(formData[dmmfPropertyName])) { + parsedData[dmmfPropertyName] = JSON.parse(formData[dmmfPropertyName] as string) as ModelWithoutRelationships[typeof dmmfPropertyName]; + } else { + parsedData[dmmfPropertyName] = null as ModelWithoutRelationships[typeof dmmfPropertyName]; + } + } else if (dmmfPropertyType === "Int") { + const value = Number(formData[dmmfPropertyName]) as number; + parsedData[dmmfPropertyName] = isNaN(value) ? undefined : value as ModelWithoutRelationships[typeof dmmfPropertyName]; + } else if (dmmfPropertyType === "Boolean") { + parsedData[dmmfPropertyName] = (formData[dmmfPropertyName] === "on") as ModelWithoutRelationships[typeof dmmfPropertyName]; + } else if (dmmfPropertyType === "DateTime") { + parsedData[dmmfPropertyName] = (formData[dmmfPropertyName] ? new Date(formData[dmmfPropertyName]!) : null) as ModelWithoutRelationships[typeof dmmfPropertyName]; + } else { + parsedData[dmmfPropertyName] = formData[dmmfPropertyName] as ModelWithoutRelationships[typeof dmmfPropertyName]; + } + } + } + ); + return parsedData; +} + /** * Convert the form data to the format expected by Prisma * @@ -282,7 +316,7 @@ export const formattedFormData = ( const dmmfPropertyTypeTyped = dmmfPropertyType as Prisma.ModelName; const fieldValue = schema.definitions[modelName].properties[ - dmmfPropertyName as Field + dmmfPropertyName as Field ]; const model = models.find((model) => model.name === dmmfPropertyType); const formatId = (value?: string) => diff --git a/packages/next-admin/src/utils/validator.ts b/packages/next-admin/src/utils/validator.ts index ff1c76a9..319a77f3 100644 --- a/packages/next-admin/src/utils/validator.ts +++ b/packages/next-admin/src/utils/validator.ts @@ -1,11 +1,11 @@ -import { EditFieldsOptions, ModelName } from "../types"; +import { EditFieldsOptions, ModelName, ModelWithoutRelationships } from "../types"; import { PropertyValidationError, ValidationError, } from "../exceptions/ValidationError"; export const validate = ( - formData: { [key: string]: string }, + formData: Partial>, fieldsOptions?: EditFieldsOptions ) => { if (!fieldsOptions) { @@ -18,7 +18,7 @@ export const validate = ( if (fieldsOptions[property]?.validate) { const validation = fieldsOptions[property]!.validate!( // @ts-ignore - formData[property as string] + formData[property] ); if (validation !== true) { From 9920a661a4447e61e26e64e38c21aace8cb30c49 Mon Sep 17 00:00:00 2001 From: Thibault Date: Mon, 4 Sep 2023 16:40:18 +0200 Subject: [PATCH 008/273] test(e2e): user validation --- .gitignore | 3 +++ CONTRIBUTING.md | 11 +++++++++++ apps/example/e2e/crud.spec.ts | 19 ++++++++++++++----- apps/example/e2e/utils.ts | 7 ++++++- 4 files changed, 34 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 849425fe..84c9733d 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,6 @@ yarn-error.log* # turbo .turbo + +# E2E +test-results \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 00138c5d..1686e743 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -38,3 +38,14 @@ docker-compose up cd apps/example && yarn database ``` +### E2E + +Tests are using Playwright to test directly with a browser. + +You can write and debug tests easily with this commande + +``` +cd apps/example +npx playwright install +npx playwright test --ui +``` \ No newline at end of file diff --git a/apps/example/e2e/crud.spec.ts b/apps/example/e2e/crud.spec.ts index d7723419..25d3c2d5 100644 --- a/apps/example/e2e/crud.spec.ts +++ b/apps/example/e2e/crud.spec.ts @@ -1,11 +1,11 @@ -import { test } from '@playwright/test'; -import { createItem, deleteItem, readItem, updateItem } from './utils'; - -export const models = ['user', 'Post', 'Category'] as const +import { test } from "@playwright/test"; +import { createItem, deleteItem, readItem, updateItem } from "./utils"; +export const models = ["user", "Post", "Category"] as const; models.forEach((model) => { let id: string; + test.describe.serial(`crud ${model}`, () => { test(`create ${model}`, async ({ page }) => { id = await createItem(model, page); @@ -20,8 +20,17 @@ models.forEach((model) => { }); test(`delete ${model}`, async ({ page }) => { - await deleteItem(model, page, id) + await deleteItem(model, page, id); }); }); }); +test.describe("user validation", () => { + test(`user create error`, async ({ page }) => { + await page.goto(`${process.env.BASE_URL}/user/new`); + await page.fill('input[id="email"]', "invalidemail"); + await page.click('button:has-text("Submit")'); + await page.waitForURL(`${process.env.BASE_URL}/user/*`); + await page.getByText("Invalid email"); + }); +}); diff --git a/apps/example/e2e/utils.ts b/apps/example/e2e/utils.ts index 6624d91c..f69e4f8f 100644 --- a/apps/example/e2e/utils.ts +++ b/apps/example/e2e/utils.ts @@ -39,6 +39,12 @@ const dataTestUpdate: DataTest = { }, } +export const dataTestValidationError: DataTest = { + user: { + email: 'my-user+e2e', + } +} + export const createItem = async (model: ModelName, page: Page): Promise => { await page.goto(`${process.env.BASE_URL}/${model}`); await page.getByRole('button', { name: 'Add' }).click(); @@ -72,7 +78,6 @@ export const updateItem = async (model: ModelName, page: Page, id: string) => { await page.click('button:has-text("Submit")'); await page.waitForURL(`${process.env.BASE_URL}/${model}/*`) await readForm(model, page, dataTestUpdate); - } From f28aa3f10027fd71752b702ea0ba75643b574047 Mon Sep 17 00:00:00 2001 From: Thibault Date: Mon, 4 Sep 2023 16:56:57 +0200 Subject: [PATCH 009/273] fix: validation crash --- apps/example/e2e/crud.spec.ts | 2 +- apps/example/e2e/utils.ts | 6 ---- .../pages/admindemo/[[...nextadmin]].tsx | 2 -- packages/next-admin/src/router.tsx | 31 +++++++------------ 4 files changed, 12 insertions(+), 29 deletions(-) diff --git a/apps/example/e2e/crud.spec.ts b/apps/example/e2e/crud.spec.ts index 25d3c2d5..9e872e2d 100644 --- a/apps/example/e2e/crud.spec.ts +++ b/apps/example/e2e/crud.spec.ts @@ -31,6 +31,6 @@ test.describe("user validation", () => { await page.fill('input[id="email"]', "invalidemail"); await page.click('button:has-text("Submit")'); await page.waitForURL(`${process.env.BASE_URL}/user/*`); - await page.getByText("Invalid email"); + await test.expect(page.getByText('Invalid email')).toBeVisible(); }); }); diff --git a/apps/example/e2e/utils.ts b/apps/example/e2e/utils.ts index f69e4f8f..35360873 100644 --- a/apps/example/e2e/utils.ts +++ b/apps/example/e2e/utils.ts @@ -39,12 +39,6 @@ const dataTestUpdate: DataTest = { }, } -export const dataTestValidationError: DataTest = { - user: { - email: 'my-user+e2e', - } -} - export const createItem = async (model: ModelName, page: Page): Promise => { await page.goto(`${process.env.BASE_URL}/${model}`); await page.getByRole('button', { name: 'Add' }).click(); diff --git a/apps/example/pages/admindemo/[[...nextadmin]].tsx b/apps/example/pages/admindemo/[[...nextadmin]].tsx index 6959be16..153a1e4f 100644 --- a/apps/example/pages/admindemo/[[...nextadmin]].tsx +++ b/apps/example/pages/admindemo/[[...nextadmin]].tsx @@ -55,8 +55,6 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { posts: { search: true, display: true, - - }, }, }, diff --git a/packages/next-admin/src/router.tsx b/packages/next-admin/src/router.tsx index 6ffb3403..d4d9e230 100644 --- a/packages/next-admin/src/router.tsx +++ b/packages/next-admin/src/router.tsx @@ -339,7 +339,8 @@ export const nextAdminRouter = async ( error.constructor.name === PrismaClientKnownRequestError.name || error.name === "ValidationError" ) { - let data; + let data = parsedFormData; + if (resourceId !== undefined) { // @ts-expect-error data = await prisma[resource].findUnique({ @@ -347,25 +348,14 @@ export const nextAdminRouter = async ( select: selectedFields, }); data = flatRelationInData(data, resource); + } - // TODO This could be improved by merging form values but it's breaking stuff - if (error.name === "ValidationError") { - error.errors.map((error: any) => { - data[error.property] = formData[error.property] - }) - } - - return { - props: { - ...defaultProps, - resource, - data, - schema, - dmmfSchema: dmmfSchema?.fields, - error: error.message, - validation: error.errors, - }, - }; + // TODO This could be improved by merging form values but it's breaking stuff + if (error.name === "ValidationError") { + error.errors.map((error: any) => { + // @ts-expect-error + data[error.property] = formData[error.property] + }) } return { @@ -375,7 +365,8 @@ export const nextAdminRouter = async ( schema, dmmfSchema: dmmfSchema?.fields, error: error.message, - data: formData, + validation: error.errors, + data, }, }; } From d136331b175423a777778b487b3a7bbf30b42407 Mon Sep 17 00:00:00 2001 From: Thibault Date: Mon, 4 Sep 2023 17:23:43 +0200 Subject: [PATCH 010/273] chore: add changeset --- .changeset/fix-data-validation.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fix-data-validation.md diff --git a/.changeset/fix-data-validation.md b/.changeset/fix-data-validation.md new file mode 100644 index 00000000..cddc8cd5 --- /dev/null +++ b/.changeset/fix-data-validation.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": patch +--- + +fix: validation crash and improve typing \ No newline at end of file From 52d5838f9cc3bf765ddf8625051538d8649fbfcf Mon Sep 17 00:00:00 2001 From: shinework Date: Thu, 7 Sep 2023 11:23:36 +0200 Subject: [PATCH 011/273] improve ui and demo --- .env | 2 +- .prettierrc | 7 +- apps/docs/pages/index.mdx | 2 +- apps/example/e2e/crud.spec.ts | 8 +- apps/example/e2e/utils.ts | 295 ++++++++++-------- .../{admindemo => admin}/[[...nextadmin]].tsx | 44 ++- apps/example/pages/index.tsx | 2 +- .../prisma/json-schema/json-schema.json | 8 +- .../migration.sql | 36 +++ apps/example/prisma/schema.prisma | 6 +- package.json | 1 - packages/next-admin/src/components/Form.tsx | 46 +-- .../next-admin/src/components/TableHead.tsx | 6 +- .../next-admin/src/components/radix/Table.tsx | 2 +- .../next-admin/src/utils/validator.test.ts | 2 +- 15 files changed, 275 insertions(+), 192 deletions(-) rename apps/example/pages/{admindemo => admin}/[[...nextadmin]].tsx (82%) create mode 100644 apps/example/prisma/migrations/20230907085437_rename_user_model/migration.sql diff --git a/.env b/.env index 275564bc..38b15b9d 100644 --- a/.env +++ b/.env @@ -5,6 +5,6 @@ # See the documentation for all the connection string options: https://pris.ly/d/connection-strings DATABASE_URL="postgresql://next-admin:next-admin@localhost:5432/next-admin?schema=public" -BASE_URL="http://localhost:3000/admindemo" +BASE_URL="http://localhost:3000/admin" BASE_DOMAIN="http://localhost:3000" ``` diff --git a/.prettierrc b/.prettierrc index 0967ef42..08b4dea6 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1 +1,6 @@ -{} +{ + "jsxSingleQuote": false, + "arrowParens": "always", + "printWidth": 80, + "trailingComma": "es5" +} diff --git a/apps/docs/pages/index.mdx b/apps/docs/pages/index.mdx index accecf08..b65bbbe4 100644 --- a/apps/docs/pages/index.mdx +++ b/apps/docs/pages/index.mdx @@ -2,7 +2,7 @@ ###### `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. -Get started by following the [installation guide](/docs/getting-started) or check out the [live demo](https://next-admin-po.vercel.app/admindemo). +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) diff --git a/apps/example/e2e/crud.spec.ts b/apps/example/e2e/crud.spec.ts index 9e872e2d..b69b5171 100644 --- a/apps/example/e2e/crud.spec.ts +++ b/apps/example/e2e/crud.spec.ts @@ -1,7 +1,7 @@ import { test } from "@playwright/test"; import { createItem, deleteItem, readItem, updateItem } from "./utils"; -export const models = ["user", "Post", "Category"] as const; +export const models = ["User", "Post", "Category"] as const; models.forEach((model) => { let id: string; @@ -27,10 +27,10 @@ models.forEach((model) => { test.describe("user validation", () => { test(`user create error`, async ({ page }) => { - await page.goto(`${process.env.BASE_URL}/user/new`); + await page.goto(`${process.env.BASE_URL}/User/new`); await page.fill('input[id="email"]', "invalidemail"); await page.click('button:has-text("Submit")'); - await page.waitForURL(`${process.env.BASE_URL}/user/*`); - await test.expect(page.getByText('Invalid email')).toBeVisible(); + await page.waitForURL(`${process.env.BASE_URL}/User/*`); + await test.expect(page.getByText("Invalid email")).toBeVisible(); }); }); diff --git a/apps/example/e2e/utils.ts b/apps/example/e2e/utils.ts index 35360873..1ee4af3b 100644 --- a/apps/example/e2e/utils.ts +++ b/apps/example/e2e/utils.ts @@ -6,162 +6,181 @@ import { models } from "./crud.spec"; export const prisma = new PrismaClient(); type DataTest = { - [key in typeof models[number]]: { - [key: string]: string - } -} + [key in (typeof models)[number]]: { + [key: string]: string; + }; +}; export const dataTest: DataTest = { - user: { - email: 'my-user+e2e@premieroctet.com', - name: 'MY_USER', - }, - Post: { - title: 'MY_POST', - authorId: 'User 0 (user0@nextadmin.io)', - }, - Category: { - name: 'MY_CATEGORY', - }, -} + User: { + email: "my-user+e2e@premieroctet.com", + name: "MY_USER", + }, + Post: { + title: "MY_POST", + authorId: "User 0 (user0@nextadmin.io)", + }, + Category: { + name: "MY_CATEGORY", + }, +}; const dataTestUpdate: DataTest = { - user: { - email: 'update-my-user+e2e@premieroctet.com', - name: 'UPDATE_MY_USER', - }, - Post: { - title: 'UPDATE_MY_POST', - authorId: 'User 1 (user1@nextadmin.io)', - }, - Category: { - name: 'UPDATE_MY_CATEGORY', - }, -} - -export const createItem = async (model: ModelName, page: Page): Promise => { - await page.goto(`${process.env.BASE_URL}/${model}`); - await page.getByRole('button', { name: 'Add' }).click(); - await page.waitForURL(`${process.env.BASE_URL}/${model}/new`); - await fillForm(model, page, dataTest); - await page.click('button:has-text("Submit")'); - await page.waitForURL(`${process.env.BASE_URL}/${model}/*`); - const url = page.url(); - const id = url.split('/').pop(); - expect(Number(id)).not.toBeNaN(); - return id!; -} + User: { + email: "update-my-user+e2e@premieroctet.com", + name: "UPDATE_MY_USER", + }, + Post: { + title: "UPDATE_MY_POST", + authorId: "User 1 (user1@nextadmin.io)", + }, + Category: { + name: "UPDATE_MY_CATEGORY", + }, +}; + +export const createItem = async ( + model: ModelName, + page: Page +): Promise => { + await page.goto(`${process.env.BASE_URL}/${model}`); + await page.getByRole("button", { name: "Add" }).click(); + await page.waitForURL(`${process.env.BASE_URL}/${model}/new`); + await fillForm(model, page, dataTest); + await page.click('button:has-text("Submit")'); + await page.waitForURL(`${process.env.BASE_URL}/${model}/*`); + const url = page.url(); + const id = url.split("/").pop(); + expect(Number(id)).not.toBeNaN(); + return id!; +}; export const deleteItem = async (model: ModelName, page: Page, id: string) => { - page.on('dialog', async dialog => dialog.accept()); - await page.goto(`${process.env.BASE_URL}/${model}/${id}`); - await page.click('button:has-text("Delete")'); - await page.waitForURL(`${process.env.BASE_URL}/${model}`); -} + page.on("dialog", async (dialog) => dialog.accept()); + await page.goto(`${process.env.BASE_URL}/${model}/${id}`); + await page.click('button:has-text("Delete")'); + await page.waitForURL(`${process.env.BASE_URL}/${model}`); +}; export const readItem = async (model: ModelName, page: Page, id: string) => { - await page.goto(`${process.env.BASE_URL}/${model}/${id}`); - await page.waitForURL(`${process.env.BASE_URL}/${model}/*`); - await readForm(model, page, dataTest); -} + await page.goto(`${process.env.BASE_URL}/${model}/${id}`); + await page.waitForURL(`${process.env.BASE_URL}/${model}/*`); + await readForm(model, page, dataTest); +}; export const updateItem = async (model: ModelName, page: Page, id: string) => { - await page.goto(`${process.env.BASE_URL}/${model}/${id}`); - await page.waitForURL(`${process.env.BASE_URL}/${model}/*`) - await fillForm(model, page, dataTestUpdate); - await page.click('button:has-text("Submit")'); - await page.waitForURL(`${process.env.BASE_URL}/${model}/*`) - await readForm(model, page, dataTestUpdate); -} - - -export const fillForm = async (model: ModelName, page: Page, dataTest: DataTest) => { - switch (model) { - case 'user': - await page.fill('input[id="email"]', dataTest.user.email); - await page.fill('input[id="name"]', dataTest.user.name); - break; - case 'Post': - await page.fill('input[id="title"]', dataTest.Post.title); - await page.getByLabel('authorId*').click(); - await page.getByText(dataTest.Post.authorId).click(); - break; - case 'Category': - await page.fill('input[id="name"]', dataTest.Category.name); - break; - default: - break; - } -} - -export const readForm = async (model: ModelName, page: Page, dataTest: DataTest) => { - switch (model) { - case 'user': - expect(await page.inputValue('input[id="email"]')).toBe(dataTest.user.email); - expect(await page.inputValue('input[id="name"]')).toBe(dataTest.user.name); - break; - case 'Post': - expect(await page.inputValue('input[id="title"]')).toBe(dataTest.Post.title); - expect(await page.inputValue('input[id="authorId"]')).toBe(dataTest.Post.authorId); - break; - case 'Category': - expect(await page.inputValue('input[id="name"]')).toBe(dataTest.Category.name); - break; - default: - break; - } -} + await page.goto(`${process.env.BASE_URL}/${model}/${id}`); + await page.waitForURL(`${process.env.BASE_URL}/${model}/*`); + await fillForm(model, page, dataTestUpdate); + await page.click('button:has-text("Submit")'); + await page.waitForURL(`${process.env.BASE_URL}/${model}/*`); + await readForm(model, page, dataTestUpdate); +}; + +export const fillForm = async ( + model: ModelName, + page: Page, + dataTest: DataTest +) => { + switch (model) { + case "User": + await page.fill('input[id="email"]', dataTest.User.email); + await page.fill('input[id="name"]', dataTest.User.name); + break; + case "Post": + await page.fill('input[id="title"]', dataTest.Post.title); + await page.getByLabel("authorId*").click(); + await page.getByText(dataTest.Post.authorId).click(); + break; + case "Category": + await page.fill('input[id="name"]', dataTest.Category.name); + break; + default: + break; + } +}; + +export const readForm = async ( + model: ModelName, + page: Page, + dataTest: DataTest +) => { + switch (model) { + case "User": + expect(await page.inputValue('input[id="email"]')).toBe( + dataTest.User.email + ); + expect(await page.inputValue('input[id="name"]')).toBe( + dataTest.User.name + ); + break; + case "Post": + expect(await page.inputValue('input[id="title"]')).toBe( + dataTest.Post.title + ); + expect(await page.inputValue('input[id="authorId"]')).toBe( + dataTest.Post.authorId + ); + break; + case "Category": + expect(await page.inputValue('input[id="name"]')).toBe( + dataTest.Category.name + ); + break; + default: + break; + } +}; const getRows = async (page: Page) => { - const table = await page.$('table'); - const tbody = await table?.$('tbody'); - return await tbody?.$$('tr'); -} - + const table = await page.$("table"); + const tbody = await table?.$("tbody"); + return await tbody?.$$("tr"); +}; export const search = async (page: Page) => { - await page.goto(`${process.env.BASE_URL}/user`); - await page.fill('input[name="search"]', 'user0@nextadmin.io'); - await page.waitForTimeout(600); - const table = await page.$('table'); - const tbody = await table?.$('tbody'); - const rows = await tbody?.$$('tr'); - const oneRow = rows?.length === 1; - expect(oneRow).toBeTruthy(); -} + await page.goto(`${process.env.BASE_URL}/User`); + await page.fill('input[name="search"]', "user0@nextadmin.io"); + await page.waitForTimeout(600); + const table = await page.$("table"); + const tbody = await table?.$("tbody"); + const rows = await tbody?.$$("tr"); + const oneRow = rows?.length === 1; + expect(oneRow).toBeTruthy(); +}; export const sort = async (page: Page) => { - await page.goto(`${process.env.BASE_URL}/user`); - await page.click('th:has-text("email")>button'); - await page.waitForTimeout(300); - let rows = await getRows(page); - let firstRow = await rows?.[0]?.innerText(); - expect(firstRow).toContain('user0@nextadmin.io'); - - await page.click('th:has-text("email")>button'); - await page.waitForTimeout(300); - rows = await getRows(page); - firstRow = await rows?.[0]?.innerText(); - expect(firstRow).toContain('user9@nextadmin.io'); -} + await page.goto(`${process.env.BASE_URL}/User`); + await page.click('th:has-text("email")>button'); + await page.waitForTimeout(300); + let rows = await getRows(page); + let firstRow = await rows?.[0]?.innerText(); + expect(firstRow).toContain("user0@nextadmin.io"); + + await page.click('th:has-text("email")>button'); + await page.waitForTimeout(300); + rows = await getRows(page); + firstRow = await rows?.[0]?.innerText(); + expect(firstRow).toContain("user9@nextadmin.io"); +}; export const pagination = async (page: Page) => { - await page.goto(`${process.env.BASE_URL}/user`); - await paginationPerPage(page, 10); -} + await page.goto(`${process.env.BASE_URL}/User`); + await paginationPerPage(page, 10); +}; export const paginationPerPage = async (page: Page, itemPerPage: number) => { - const numberOfItems = 25; - const numberOfPages = Math.ceil(numberOfItems / itemPerPage); - for (let i = 1; i <= numberOfPages; i++) { - await page.getByRole('button', { name: i.toString() }).click(); - await page.waitForTimeout(300); - let rows = await getRows(page); - if (i === numberOfPages) { - expect(rows?.length).toBe(numberOfItems % itemPerPage); - } else { - expect(rows?.length).toBe(itemPerPage); - } + const numberOfItems = 25; + const numberOfPages = Math.ceil(numberOfItems / itemPerPage); + for (let i = 1; i <= numberOfPages; i++) { + await page.getByRole("button", { name: i.toString() }).click(); + await page.waitForTimeout(300); + let rows = await getRows(page); + if (i === numberOfPages) { + expect(rows?.length).toBe(numberOfItems % itemPerPage); + } else { + expect(rows?.length).toBe(itemPerPage); } - await page.getByRole('button', { name: '1' }).click(); -} \ No newline at end of file + } + await page.getByRole("button", { name: "1" }).click(); +}; diff --git a/apps/example/pages/admindemo/[[...nextadmin]].tsx b/apps/example/pages/admin/[[...nextadmin]].tsx similarity index 82% rename from apps/example/pages/admindemo/[[...nextadmin]].tsx rename to apps/example/pages/admin/[[...nextadmin]].tsx index 153a1e4f..1c487b08 100644 --- a/apps/example/pages/admindemo/[[...nextadmin]].tsx +++ b/apps/example/pages/admin/[[...nextadmin]].tsx @@ -3,25 +3,35 @@ import { GetServerSideProps, GetServerSidePropsResult } from "next"; import { prisma } from "../../prisma"; import schema from "../../prisma/json-schema/json-schema.json"; import "@premieroctet/next-admin/dist/styles.css"; -import { AdminComponentProps, NextAdmin, NextAdminOptions } from "@premieroctet/next-admin"; +import { + AdminComponentProps, + NextAdmin, + NextAdminOptions, +} from "@premieroctet/next-admin"; import Dashboard from "../../components/Dashboard"; export default function Admin(props: AdminComponentProps) { - return { - return {user.role as string}; + return ( + { + return {user.role as string}; + }, + }, }, - } - } - } - } - } - }} />; + }, + }, + }, + }} + /> + ); } export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { @@ -30,9 +40,9 @@ export const getServerSideProps: GetServerSideProps = async ({ req, res }) => { ); const options: NextAdminOptions = { - basePath: "/admindemo", + basePath: "/admin", model: { - user: { + User: { toString: (user) => `${user.name} (${user.email})`, list: { fields: { diff --git a/apps/example/pages/index.tsx b/apps/example/pages/index.tsx index f2340ed4..4944a20f 100644 --- a/apps/example/pages/index.tsx +++ b/apps/example/pages/index.tsx @@ -17,7 +17,7 @@ export default function Web() { started now! Explore Next Admin diff --git a/apps/example/prisma/json-schema/json-schema.json b/apps/example/prisma/json-schema/json-schema.json index 6bbccf64..1f7e75ce 100644 --- a/apps/example/prisma/json-schema/json-schema.json +++ b/apps/example/prisma/json-schema/json-schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "definitions": { - "user": { + "User": { "type": "object", "properties": { "id": { @@ -80,7 +80,7 @@ "default": false }, "author": { - "$ref": "#/definitions/user" + "$ref": "#/definitions/User" }, "categories": { "type": "array", @@ -115,7 +115,7 @@ "user": { "anyOf": [ { - "$ref": "#/definitions/user" + "$ref": "#/definitions/User" }, { "type": "null" @@ -191,7 +191,7 @@ "type": "object", "properties": { "user": { - "$ref": "#/definitions/user" + "$ref": "#/definitions/User" }, "post": { "$ref": "#/definitions/Post" diff --git a/apps/example/prisma/migrations/20230907085437_rename_user_model/migration.sql b/apps/example/prisma/migrations/20230907085437_rename_user_model/migration.sql new file mode 100644 index 00000000..83df99c9 --- /dev/null +++ b/apps/example/prisma/migrations/20230907085437_rename_user_model/migration.sql @@ -0,0 +1,36 @@ +/* + Warnings: + + - You are about to drop the `user` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "Post" DROP CONSTRAINT "Post_authorId_fkey"; + +-- DropForeignKey +ALTER TABLE "Profile" DROP CONSTRAINT "Profile_userId_fkey"; + +-- DropTable +DROP TABLE "user"; + +-- CreateTable +CREATE TABLE "User" ( + "id" SERIAL NOT NULL, + "email" TEXT NOT NULL, + "name" TEXT, + "birthDate" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "role" "Role" NOT NULL DEFAULT 'USER', + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- AddForeignKey +ALTER TABLE "Post" ADD CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Profile" ADD CONSTRAINT "Profile_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/example/prisma/schema.prisma b/apps/example/prisma/schema.prisma index 587a76a1..fd2b1c76 100644 --- a/apps/example/prisma/schema.prisma +++ b/apps/example/prisma/schema.prisma @@ -19,7 +19,7 @@ enum Role { USER ADMIN } -model user { +model User { id Int @id @default(autoincrement()) email String @unique name String? @@ -36,7 +36,7 @@ model Post { title String content String? published Boolean @default(false) - author user @relation("author", fields: [authorId], references: [id]) + author User @relation("author", fields: [authorId], references: [id]) authorId Int categories Category[] @relation("category") // implicit Many-to-many relation comments post_comment[] @relation("comments") // One-to-many relation @@ -45,7 +45,7 @@ model Post { model Profile { id Int @id @default(autoincrement()) bio String? - user user? @relation("profile", fields: [userId], references: [id]) + user User? @relation("profile", fields: [userId], references: [id]) userId Int? @unique createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt diff --git a/package.json b/package.json index d66e2bb4..c22f9c72 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,6 @@ "test": "turbo run test", "test:e2e": "dotenv turbo test:e2e", "database": "dotenv turbo run database", - "format": "prettier --write \"**/*.{ts,tsx,md}\"", "publish-package": "turbo run build && changeset publish" }, diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx index 36bece3d..b3cde4d7 100644 --- a/packages/next-admin/src/components/Form.tsx +++ b/packages/next-admin/src/components/Form.tsx @@ -36,19 +36,19 @@ export type FormProps = { schema: any; dmmfSchema: Prisma.DMMF.Field[]; resource: ModelName; - validation?: PropertyValidationError[] + validation?: PropertyValidationError[]; }; -const fields: CustomForm['props']['fields'] = { +const fields: CustomForm["props"]["fields"] = { ArrayField, }; -const widgets: CustomForm['props']['widgets'] = { +const widgets: CustomForm["props"]["widgets"] = { SelectWidget: SelectWidget, CheckboxWidget: CheckboxWidget, }; -const templates: CustomForm['props']['templates'] = { +const templates: CustomForm["props"]["templates"] = { FieldTemplate: (props: FieldTemplateProps) => { const { id, @@ -64,7 +64,7 @@ const templates: CustomForm['props']['templates'] = { return (