diff --git a/.changeset/fair-jokes-grab.md b/.changeset/fair-jokes-grab.md new file mode 100644 index 00000000..fed3ff4f --- /dev/null +++ b/.changeset/fair-jokes-grab.md @@ -0,0 +1,5 @@ +--- +"@premieroctet/next-admin": minor +--- + +feat: revamp design diff --git a/apps/docs/components/Logo.js b/apps/docs/components/Logo.js index 91b93a8c..07c779e9 100644 --- a/apps/docs/components/Logo.js +++ b/apps/docs/components/Logo.js @@ -34,7 +34,7 @@ const Logo = ({ width }) => ( } }`} - + Note that the `search` property is only available for `scalar` fields. diff --git a/apps/docs/pages/docs/i18n.mdx b/apps/docs/pages/docs/i18n.mdx index cf37c1e3..f2a065eb 100644 --- a/apps/docs/pages/docs/i18n.mdx +++ b/apps/docs/pages/docs/i18n.mdx @@ -4,22 +4,23 @@ Next Admin supports i18n with the `translations` prop of the `NextAdmin` compone The following keys are accepted: -| 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.empty.label | The text displayed when there is no row in the list | No \{\{resource\}\} found | -| 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.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 | -| actions.label | The text displayed in the dropdown button for the actions list | Action | -| actions.delete.label | The text displayed for the default delete action in the actions dropdown | Delete | +| 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.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.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 | +| actions.label | The text displayed in the dropdown button for the actions list | Action | +| actions.delete.label | The text displayed for the default delete action in the actions dropdown | Delete | There is two ways to translate these default keys, provide a function named `getMessages` inside the options or provide `translations` props to `NextAdmin` component. diff --git a/apps/docs/public/logo.svg b/apps/docs/public/logo.svg index 990e2ca9..21a409c6 100644 --- a/apps/docs/public/logo.svg +++ b/apps/docs/public/logo.svg @@ -25,7 +25,7 @@ } } - + { const mainLayoutProps = getMainLayoutProps({ options, isAppDir: true }); @@ -11,40 +10,44 @@ const CustomPage = async () => { const totalPosts = await prisma.post.count(); const totalCategories = await prisma.category.count(); + const stats = [ + { name: "Total Users", stat: totalUsers }, + { name: "Total Posts", stat: totalPosts }, + { name: "Total Categories", stat: totalCategories }, + ]; + return ( -
+

- Custom page + Dashboard

-

- Custom queries -

-

Total Users: {totalUsers}

-

Total Posts: {totalPosts}

-

Total Categories: {totalCategories}

-
-
-

- Custom actions -

-
- -
+
+
+ {stats.map((item) => ( +
+
+ {item.name} +
+
+ {item.stat} +
+
+ ))} +
+
diff --git a/apps/example/components/Dashboard.tsx b/apps/example/components/Dashboard.tsx index 3f3de62c..4dffc9a5 100644 --- a/apps/example/components/Dashboard.tsx +++ b/apps/example/components/Dashboard.tsx @@ -1,10 +1,10 @@ -import Image from "next/image"; import { Divider, Text, Title } from "@tremor/react"; +import Image from "next/image"; const Dashboard = () => { return ( -
-
+
+
Next Admin diff --git a/apps/example/e2e/003-custom_pages.spec.ts b/apps/example/e2e/003-custom_pages.spec.ts index 9237d019..aef48a32 100644 --- a/apps/example/e2e/003-custom_pages.spec.ts +++ b/apps/example/e2e/003-custom_pages.spec.ts @@ -1,21 +1,18 @@ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; test.describe("Custom pages", () => { test("Custom page should be visible and clickable", async ({ page }) => { await page.goto(process.env.BASE_URL!); - await page.click(`a[href$="/custom"]`); await expect( page.locator("h1", { - hasText: "Custom page", + hasText: "Dashboard", }) ).toBeVisible(); - await page.getByText("Create random post").click(); - - await page.waitForURL((url) => url.pathname.includes("/post/")); - - await expect(page.getByText("Random post created")).toBeVisible(); + // await page.getByText("Create random post").click(); + // await page.waitForURL((url) => url.pathname.includes("/post/")); + // await expect(page.getByText("Random post created")).toBeVisible(); }); }); diff --git a/apps/example/e2e/004-custom_actions.spec.ts b/apps/example/e2e/004-custom_actions.spec.ts index 43898729..4a80f286 100644 --- a/apps/example/e2e/004-custom_actions.spec.ts +++ b/apps/example/e2e/004-custom_actions.spec.ts @@ -1,4 +1,4 @@ -import { test, expect } from "@playwright/test"; +import { expect, test } from "@playwright/test"; test.describe("User's custom actions", () => { test("Submit email", async ({ page }) => { @@ -6,7 +6,7 @@ test.describe("User's custom actions", () => { await page.goto(`${process.env.BASE_URL}/user`); await expect(page.getByTestId("actions-dropdown")).not.toBeVisible(); - const checkboxes = page.locator('table input[type="checkbox"]'); + const checkboxes = page.locator('table button[role="checkbox"]'); await checkboxes.first().check(); await checkboxes.nth(2).check(); @@ -24,7 +24,7 @@ test.describe("User's custom actions", () => { await page.goto(`${process.env.BASE_URL}/user?page=3`); await expect(page.getByTestId("actions-dropdown")).not.toBeVisible(); - const checkboxes = page.locator('table input[type="checkbox"]'); + const checkboxes = page.locator('table button[role="checkbox"]'); await checkboxes.nth(4).check(); await checkboxes.last().check(); @@ -38,6 +38,7 @@ test.describe("User's custom actions", () => { .getByTestId("actions-dropdown-content") .getByText("Delete") .click(); + await page.waitForURL((url) => !!url.searchParams.get("message")); await expect(page.getByText("Deleted successfully")).toBeVisible(); await expect(page.locator("table tbody tr")).toHaveCount(3); diff --git a/apps/example/e2e/utils.ts b/apps/example/e2e/utils.ts index cbfae2e3..f30ffaaf 100644 --- a/apps/example/e2e/utils.ts +++ b/apps/example/e2e/utils.ts @@ -1,6 +1,6 @@ import { Page, expect } from "@playwright/test"; -import { PrismaClient } from "@prisma/client"; import { ModelName } from "@premieroctet/next-admin"; +import { PrismaClient } from "@prisma/client"; import { models } from "./001-crud.spec"; export const prisma = new PrismaClient(); @@ -44,15 +44,20 @@ export const createItem = async ( page: Page ): Promise => { await page.goto(`${process.env.BASE_URL}/${model}`); - await page.getByRole("button", { name: "Add" }).click(); + await page.getByTestId("add-new-button").click(); await page.waitForURL(`${process.env.BASE_URL}/${model}/new`); + await fillForm(model, page, dataTest); - await page.click('button:has-text("Save and continue editing")'); + + await page.click('button[type="submit"]'); await page.waitForURL((url) => !url.pathname.endsWith("/new")); + const url = new URL(page.url()); const id = url.pathname.split("/").pop(); + expect(Number(id)).not.toBeNaN(); expect(page.getByText("Created successfully")).toBeDefined(); + return id!; }; @@ -161,7 +166,7 @@ const getRows = async (page: Page) => { 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); + await page.waitForURL((url) => !!url.searchParams.get("search")); const table = await page.$("table"); const tbody = await table?.$("tbody"); const rows = await tbody?.$$("tr"); diff --git a/apps/example/options.tsx b/apps/example/options.tsx index 0db43143..f8f1ab9e 100644 --- a/apps/example/options.tsx +++ b/apps/example/options.tsx @@ -3,7 +3,7 @@ import DatePicker from "./components/DatePicker"; export const options: NextAdminOptions = { basePath: "/admin", - title: "Next Admin Example (App dir)", + title: "⚡️ My Admin", model: { User: { toString: (user) => `${user.name} (${user.email})`, @@ -15,6 +15,7 @@ export const options: NextAdminOptions = { list: { display: ["id", "name", "email", "posts", "role", "birthDate"], search: ["name", "email", "role"], + copy: ["email"], fields: { role: { formatter: (role) => { @@ -161,7 +162,7 @@ export const options: NextAdminOptions = { pages: { "/custom": { title: "Custom page", - icon: "AdjustmentsHorizontalIcon", + icon: "PresentationChartBarIcon", }, }, sidebar: { diff --git a/apps/example/pageRouterOptions.tsx b/apps/example/pageRouterOptions.tsx index 7d8d880e..4c34c888 100644 --- a/apps/example/pageRouterOptions.tsx +++ b/apps/example/pageRouterOptions.tsx @@ -4,7 +4,7 @@ import DatePicker from "./components/DatePicker"; export const options: NextAdminOptions = { basePath: "/pagerouter/admin", - title: "Next Admin Example (Pages dir)", + title: "⚡️ My Admin", model: { User: { toString: (user) => `${user.name} (${user.email})`, diff --git a/apps/example/pages/pagerouter/admin/[[...nextadmin]].tsx b/apps/example/pages/pagerouter/admin/[[...nextadmin]].tsx index 382aef7a..436031ed 100644 --- a/apps/example/pages/pagerouter/admin/[[...nextadmin]].tsx +++ b/apps/example/pages/pagerouter/admin/[[...nextadmin]].tsx @@ -1,6 +1,5 @@ import { AdminComponentProps, NextAdmin } from "@premieroctet/next-admin"; import { GetServerSideProps, GetServerSidePropsResult } from "next"; -import Dashboard from "../../../components/Dashboard"; import { options } from "../../../pageRouterOptions"; import { prisma } from "../../../prisma"; import schema from "../../../prisma/json-schema/json-schema.json"; @@ -12,11 +11,10 @@ export default function Admin(props: AdminComponentProps) { return ( { + const stats = [ + { name: "Total Users", stat: totalUsers }, + { name: "Total Posts", stat: totalPosts }, + { name: "Total Categories", stat: totalCategories }, + ]; + return ( -
+

- Custom page + Dashboard

-

- Custom queries -

-

Total Users: {totalUsers}

-

Total Posts: {totalPosts}

-

Total Categories: {totalCategories}

-
-
-

- Custom actions -

-
- -
+
+
+ {stats.map((item) => ( +
+
+ {item.name} +
+
+ {item.stat} +
+
+ ))} +
+
diff --git a/apps/example/playwright.config.ts b/apps/example/playwright.config.ts index f7109804..58a6a99e 100644 --- a/apps/example/playwright.config.ts +++ b/apps/example/playwright.config.ts @@ -3,4 +3,7 @@ import { defineConfig } from "@playwright/test"; export default defineConfig({ testDir: "e2e", workers: 1, + use: { + viewport: { width: 1920, height: 1080 }, + }, }); diff --git a/packages/next-admin/package.json b/packages/next-admin/package.json index a48f5b2a..d04d9a66 100644 --- a/packages/next-admin/package.json +++ b/packages/next-admin/package.json @@ -22,6 +22,7 @@ "@headlessui/react": "^1.7.13", "@heroicons/react": "^2.0.18", "@picocss/pico": "^1.5.7", + "@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-dropdown-menu": "^2.0.6", "@radix-ui/react-select": "^1.2.2", "@radix-ui/react-switch": "^1.0.3", diff --git a/packages/next-admin/src/components/ActionsDropdown.tsx b/packages/next-admin/src/components/ActionsDropdown.tsx index c3511bc6..61cb87b8 100644 --- a/packages/next-admin/src/components/ActionsDropdown.tsx +++ b/packages/next-admin/src/components/ActionsDropdown.tsx @@ -1,5 +1,12 @@ +import { Transition } from "@headlessui/react"; +import { + ChevronDownIcon, + EllipsisVerticalIcon, +} from "@heroicons/react/24/outline"; import clsx from "clsx"; -import { ChevronDownIcon } from "@heroicons/react/24/outline"; +import { Fragment, useState } from "react"; +import { useI18n } from "../context/I18nContext"; +import { useAction } from "../hooks/useAction"; import { ModelAction, ModelName } from "../types"; import { Dropdown, @@ -8,10 +15,6 @@ import { DropdownItem, DropdownTrigger, } from "./radix/Dropdown"; -import { useAction } from "../hooks/useAction"; -import { useI18n } from "../context/I18nContext"; -import { Fragment, useState } from "react"; -import { Transition } from "@headlessui/react"; type Props = { actions: ModelAction[]; @@ -42,9 +45,20 @@ const ActionsDropdown = ({ data-testid="actions-dropdown" > @@ -65,8 +79,9 @@ const ActionsDropdown = ({ return ( onActionClick(action)} > diff --git a/packages/next-admin/src/components/Breadcrumb.tsx b/packages/next-admin/src/components/Breadcrumb.tsx new file mode 100644 index 00000000..b2b4de42 --- /dev/null +++ b/packages/next-admin/src/components/Breadcrumb.tsx @@ -0,0 +1,60 @@ +import { HomeIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; +import { useConfig } from "../context/ConfigContext"; +import { ModelIcon } from "../types"; +import ResourceIcon from "./common/ResourceIcon"; + +export default function Breadcrumb({ + breadcrumbItems, +}: { + breadcrumbItems: { + href: string; + label: string; + icon?: ModelIcon; + current?: boolean; + }[]; +}) { + const { basePath } = useConfig(); + + return ( + + ); +} diff --git a/packages/next-admin/src/components/Cell.tsx b/packages/next-admin/src/components/Cell.tsx index 291a90ae..c6dacf1d 100644 --- a/packages/next-admin/src/components/Cell.tsx +++ b/packages/next-admin/src/components/Cell.tsx @@ -9,9 +9,10 @@ import { useConfig } from "../context/ConfigContext"; type Props = { cell: ListDataFieldValue; formatter?: (cell: any) => ReactNode; + copyable?: boolean; }; -export default function Cell({ cell, formatter }: Props) { +export default function Cell({ cell, formatter, copyable }: Props) { const { basePath } = useConfig(); let cellValue = cell?.__nextadmin_formatted; @@ -38,7 +39,7 @@ export default function Cell({ cell, formatter }: Props) { className="hover:underline cursor-pointer text-nextadmin-primary-700 hover:text-nextadmin-primary-900 font-semibold flex items-center gap-1" > {cellValue} - + {copyable && } ); } else if (cell.type === "count") { @@ -51,21 +52,21 @@ export default function Cell({ cell, formatter }: Props) { return (

{cellValue}

- + {copyable && }
); } else if (cell.type === "scalar" && typeof cell.value === "string") { return (

{cellValue}

- + {copyable && }
); } else if (cell.type === "scalar" && typeof cell.value === "number") { return (

{cellValue}

- + {copyable && }
); } else if (cell.type === "scalar" && typeof cell.value === "boolean") { @@ -87,7 +88,7 @@ export default function Cell({ cell, formatter }: Props) { return (
{JSON.stringify(cellValue)} - + {copyable && }
); } diff --git a/packages/next-admin/src/components/Dashboard.tsx b/packages/next-admin/src/components/Dashboard.tsx index e416bc63..2a3e34ea 100644 --- a/packages/next-admin/src/components/Dashboard.tsx +++ b/packages/next-admin/src/components/Dashboard.tsx @@ -1,4 +1,6 @@ "use client"; + +import { useEffect } from "react"; import { useConfig } from "../context/ConfigContext"; import { useRouterInternal } from "../hooks/useRouterInternal"; import { ModelName } from "../types"; @@ -11,9 +13,11 @@ const Dashboard = ({ resources }: DashboardProps) => { const { basePath } = useConfig(); const { router } = useRouterInternal(); - router.replace({ - pathname: `${basePath}/${resources[0]}`, - }); + useEffect(() => { + router.replace({ + pathname: `${basePath}/${resources[0]}`, + }); + }, [router, basePath, resources]); return null; }; diff --git a/packages/next-admin/src/components/DataTable.tsx b/packages/next-admin/src/components/DataTable.tsx index 555ff9b9..50502190 100644 --- a/packages/next-admin/src/components/DataTable.tsx +++ b/packages/next-admin/src/components/DataTable.tsx @@ -7,12 +7,21 @@ import { useReactTable, } from "@tanstack/react-table"; +import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; import { useConfig } from "../context/ConfigContext"; import { useI18n } from "../context/I18nContext"; import { useRouterInternal } from "../hooks/useRouterInternal"; -import { Field, ListData, ListDataItem, ModelName } from "../types"; -import { Checkbox } from "./common/Checkbox"; +import { Field, ListData, ListDataItem, ModelIcon, ModelName } from "../types"; +import EmptyState from "./EmptyState"; +import Checkbox from "./radix/Checkbox"; import Button from "./radix/Button"; +import { + Dropdown, + DropdownBody, + DropdownContent, + DropdownItem, + DropdownTrigger, +} from "./radix/Dropdown"; import { Table, TableBody, @@ -30,6 +39,7 @@ interface DataTableProps { rowSelection: RowSelectionState; setRowSelection: OnChangeFn; onDelete?: (id: string | number) => void; + icon?: ModelIcon; } export function DataTable({ @@ -40,20 +50,18 @@ export function DataTable({ rowSelection, setRowSelection, onDelete, + icon, }: DataTableProps) { const { router } = useRouterInternal(); const { basePath } = useConfig(); const { t } = useI18n(); - const columnsVisibility = columns.reduce( - (acc, column) => { - // @ts-expect-error - const key = column.accessorKey as Field; - acc[key] = Object.keys(data[0]).includes(key); - return acc; - }, - {} as Record, boolean> - ); + const columnsVisibility = columns.reduce((acc, column) => { + // @ts-expect-error + const key = column.accessorKey as Field; + acc[key] = Object.keys(data[0]).includes(key); + return acc; + }, {} as Record, boolean>); const modelIdProperty = resourcesIdProperty[resource]; const checkboxColumn: ColumnDef> = { @@ -101,16 +109,35 @@ export function DataTable({ const idProperty = resourcesIdProperty[resource]; return ( - + + + + + + + + + + + + ); }, }; @@ -131,33 +158,39 @@ export function DataTable({ }); return ( -
+
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} - - ); - })} - - ))} - + {table.getRowModel().rows?.length > 0 && ( + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ); + })} + + ))} + + )} {table.getRowModel().rows?.length ? ( table.getRowModel().rows.map((row) => ( { window.location.href = `${basePath}/${resource.toLowerCase()}/${ row.original[modelIdProperty].value @@ -165,7 +198,12 @@ export function DataTable({ }} > {row.getVisibleCells().map((cell) => ( - + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} @@ -177,9 +215,7 @@ export function DataTable({ colSpan={table.getAllColumns().length} className="h-24 text-center" > -
- {t("list.empty.label", { resource })} -
+
)} diff --git a/packages/next-admin/src/components/Divider.tsx b/packages/next-admin/src/components/Divider.tsx new file mode 100644 index 00000000..bbdc02ce --- /dev/null +++ b/packages/next-admin/src/components/Divider.tsx @@ -0,0 +1,3 @@ +export default function Divider() { + return
; +} diff --git a/packages/next-admin/src/components/EmptyState.tsx b/packages/next-admin/src/components/EmptyState.tsx new file mode 100644 index 00000000..e05215c5 --- /dev/null +++ b/packages/next-admin/src/components/EmptyState.tsx @@ -0,0 +1,51 @@ +import { PlusSmallIcon } from "@heroicons/react/24/outline"; +import Link from "next/link"; +import { useConfig } from "../context/ConfigContext"; +import { useI18n } from "../context/I18nContext"; +import { ModelIcon } from "../types"; +import ResourceIcon from "./common/ResourceIcon"; +import { buttonVariants } from "./radix/Button"; + +const EmptyState = ({ + resource, + icon, +}: { + resource: string; + icon?: ModelIcon; +}) => { + const { t } = useI18n(); + const { basePath } = useConfig(); + + return ( +
+ +

+ {t("list.empty.label", { resource: resource.toLowerCase() })} +

+

+ {t("list.empty.caption", { resource: resource.toLowerCase() })} +

+
+ + + {t("list.header.add.label")} {resource.toLowerCase()} + + +
+
+ ); +}; + +export default EmptyState; diff --git a/packages/next-admin/src/components/Form.tsx b/packages/next-admin/src/components/Form.tsx index 4e17ef75..1e86ceef 100644 --- a/packages/next-admin/src/components/Form.tsx +++ b/packages/next-admin/src/components/Form.tsx @@ -1,4 +1,9 @@ "use client"; +import { + CheckCircleIcon, + InformationCircleIcon, + TrashIcon, +} from "@heroicons/react/24/outline"; import { Prisma } from "@prisma/client"; import RjsfForm from "@rjsf/core"; import { @@ -11,8 +16,13 @@ import { } from "@rjsf/utils"; import validator from "@rjsf/validator-ajv8"; import clsx from "clsx"; -import { InformationCircleIcon } from "@heroicons/react/24/outline"; -import React, { ChangeEvent, cloneElement, useMemo, useState } from "react"; +import React, { + ChangeEvent, + cloneElement, + useMemo, + useRef, + useState, +} from "react"; import { useConfig } from "../context/ConfigContext"; import { FormContext, FormProvider } from "../context/FormContext"; import { useI18n } from "../context/I18nContext"; @@ -28,18 +38,18 @@ import { } from "../types"; import { getSchemas } from "../utils/jsonSchema"; import ActionsDropdown from "./ActionsDropdown"; +import Breadcrumb from "./Breadcrumb"; import ArrayField from "./inputs/ArrayField"; -import NullField from "./inputs/NullField"; import CheckboxWidget from "./inputs/CheckboxWidget"; import DateTimeWidget from "./inputs/DateTimeWidget"; import DateWidget from "./inputs/DateWidget"; import FileWidget from "./inputs/FileWidget"; import JsonField from "./inputs/JsonField"; +import NullField from "./inputs/NullField"; import RichTextField from "./inputs/RichText/RichTextField"; import SelectWidget from "./inputs/SelectWidget"; import TextareaWidget from "./inputs/TextareaWidget"; import Button from "./radix/Button"; -import ResourceIcon from "./common/ResourceIcon"; import { TooltipContent, TooltipPortal, @@ -48,17 +58,8 @@ import { TooltipTrigger, } from "./radix/Tooltip"; -// Override Form functions to not prevent the submit class CustomForm extends RjsfForm { - onSubmit = (e: any) => { - if ( - e.nativeEvent.submitter.value === "delete" && - !confirm("Are you sure to delete this ?") - ) { - e.preventDefault(); - return false; - } - }; + onSubmit = (e: any) => {}; } export type FormProps = { @@ -107,33 +108,80 @@ const Form = ({ const { basePath } = useConfig(); const { router } = useRouterInternal(); const { t } = useI18n(); + const formRef = useRef(null); + const submitButton = (props: SubmitButtonProps) => { const { uiSchema } = props; const { norender, props: buttonProps } = getSubmitButtonOptions(uiSchema); + if (norender) { return null; } return (
+
+ {edit && ( + + )} +
- - -
- {edit && ( - )} +
); }; @@ -160,6 +208,8 @@ const Form = ({ setValidation(undefined); } + console.log("SUBMIT RESULT", result); + if (result?.deleted) { return router.replace({ pathname: `${basePath}/${resource.toLowerCase()}`, @@ -171,6 +221,7 @@ const Form = ({ }, }); } + if (result?.created) { const pathname = result?.redirect ? `${basePath}/${resource.toLowerCase()}` @@ -227,18 +278,24 @@ const Form = ({ children, schema, } = props; + const labelAlias = options?.aliases?.[id as Field] || label; let styleField = options?.edit?.styles?.[id as Field]; const tooltip = options?.edit?.fields?.[id as Field]?.tooltip; + const sanitizedClassNames = classNames ?.split(",") .filter((className) => className !== "null") .join(" "); + return ( -
+
{schema.type !== "null" && (