From 9d150b445fe17c55f5c27e63de622cb3fa455da3 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 20 Mar 2025 12:44:10 +0100 Subject: [PATCH 1/2] isolate studio --- .codesandbox/ci.json | 10 +- docs/package.json | 2 +- packages/create-toolpad-app/README.md | 24 - packages/create-toolpad-app/package.json | 52 - .../public/templates/gitignoreTemplate | 166 --- packages/create-toolpad-app/src/core.ts | 52 - packages/create-toolpad-app/src/examples.ts | 63 - .../create-toolpad-app/src/generateProject.ts | 195 --- .../src/generateStudioProject.ts | 14 - packages/create-toolpad-app/src/index.ts | 230 ---- packages/create-toolpad-app/src/package.ts | 28 - .../create-toolpad-app/src/packageType.ts | 667 ----------- packages/create-toolpad-app/src/studio.ts | 48 - .../src/templates/eslintConfig.ts | 6 - .../src/templates/gitignore.ts | 149 --- .../src/templates/indexPage.ts | 61 - .../src/templates/nextConfig.ts | 11 - .../src/templates/nextTypes.ts | 8 - .../src/templates/nextjs/auth/auth.ts | 145 --- .../src/templates/nextjs/auth/env.ts | 5 - .../src/templates/nextjs/auth/envLocal.ts | 32 - .../src/templates/nextjs/auth/middleware.ts | 10 - .../nextjs/auth/nextjs-app/actions.ts | 48 - .../nextjs/auth/nextjs-app/signInPage.ts | 64 - .../nextjs/auth/nextjs-pages/signIn.ts | 117 -- .../src/templates/nextjs/auth/prisma.ts | 11 - .../src/templates/nextjs/auth/route.ts | 6 - .../src/templates/nextjs/auth/schemaPrisma.ts | 88 -- .../src/templates/nextjs/auth/utils.ts | 14 - .../nextjs/nextjs-app/dashboardLayout.ts | 14 - .../templates/nextjs/nextjs-app/rootLayout.ts | 85 -- .../src/templates/nextjs/nextjs-pages/app.ts | 154 --- .../templates/nextjs/nextjs-pages/document.ts | 30 - .../src/templates/ordersPage.ts | 52 - .../src/templates/packageJson.ts | 111 -- .../src/templates/readme.ts | 26 - .../src/templates/studio/packageJson.ts | 16 - .../create-toolpad-app/src/templates/theme.ts | 15 - .../src/templates/tsConfig.ts | 29 - .../src/templates/vite/App.ts | 102 -- .../src/templates/vite/SessionContext.ts | 25 - .../src/templates/vite/auth/env.ts | 8 - .../src/templates/vite/auth/firebase.ts | 116 -- .../src/templates/vite/auth/firebaseConfig.ts | 34 - .../src/templates/vite/auth/signin.ts | 101 -- .../src/templates/vite/dashboardLayout.ts | 64 - .../src/templates/vite/html.ts | 12 - .../src/templates/vite/main.ts | 52 - .../src/templates/vite/viteConfig.ts | 7 - packages/create-toolpad-app/src/types.ts | 28 - packages/create-toolpad-app/src/validation.ts | 65 - packages/create-toolpad-app/src/writeFiles.ts | 22 - .../create-toolpad-app/tests/index.spec.ts | 396 ------- .../create-toolpad-app/tests/package.json | 3 - .../create-toolpad-app/tests/tsconfig.json | 6 - packages/create-toolpad-app/tsconfig.json | 21 - packages/create-toolpad-app/tsup.config.ts | 14 - packages/create-toolpad-app/vitest.config.mts | 7 - packages/toolpad-core/README.md | 20 - packages/toolpad-core/package.json | 102 -- .../toolpad-core/src/Account/Account.test.tsx | 63 - packages/toolpad-core/src/Account/Account.tsx | 257 ---- .../src/Account/AccountLocaleContext.tsx | 8 - .../src/Account/AccountPopoverFooter.tsx | 50 - .../src/Account/AccountPopoverHeader.tsx | 35 - .../src/Account/AccountPreview.test.tsx | 82 -- .../src/Account/AccountPreview.tsx | 204 ---- .../toolpad-core/src/Account/SignInButton.tsx | 58 - .../src/Account/SignOutButton.tsx | 62 - packages/toolpad-core/src/Account/index.ts | 6 - .../src/AppProvider/AppProvider.test.tsx | 25 - .../src/AppProvider/AppProvider.tsx | 289 ----- .../src/AppProvider/AppThemeProvider.tsx | 167 --- .../src/AppProvider/LocalizationProvider.tsx | 124 -- .../toolpad-core/src/AppProvider/index.ts | 2 - packages/toolpad-core/src/Crud/Create.tsx | 237 ---- packages/toolpad-core/src/Crud/Crud.test.tsx | 352 ------ packages/toolpad-core/src/Crud/Crud.tsx | 182 --- packages/toolpad-core/src/Crud/CrudForm.tsx | 388 ------ .../toolpad-core/src/Crud/CrudProvider.tsx | 74 -- packages/toolpad-core/src/Crud/Edit.tsx | 316 ----- packages/toolpad-core/src/Crud/List.tsx | 512 -------- packages/toolpad-core/src/Crud/Show.tsx | 314 ----- packages/toolpad-core/src/Crud/cache.ts | 42 - packages/toolpad-core/src/Crud/index.ts | 14 - packages/toolpad-core/src/Crud/localeText.ts | 40 - packages/toolpad-core/src/Crud/types.ts | 40 - .../src/Crud/useCachedDataSource.ts | 69 -- .../src/DashboardLayout/AppTitle.tsx | 48 - .../DashboardLayout/DashboardLayout.test.tsx | 430 ------- .../src/DashboardLayout/DashboardLayout.tsx | 594 ---------- .../DashboardSidebarSubNavigation.tsx | 368 ------ .../src/DashboardLayout/ThemeSwitcher.tsx | 78 -- .../src/DashboardLayout/ToolbarActions.tsx | 24 - .../src/DashboardLayout/ToolpadLogo.tsx | 60 - .../toolpad-core/src/DashboardLayout/index.ts | 3 - .../src/DashboardLayout/shared.ts | 1 - .../toolpad-core/src/DashboardLayout/utils.ts | 20 - .../src/PageContainer/PageContainer.test.tsx | 166 --- .../src/PageContainer/PageContainer.tsx | 140 --- .../src/PageContainer/PageHeader.tsx | 147 --- .../PageContainer/PageHeaderToolbar.test.tsx | 16 - .../src/PageContainer/PageHeaderToolbar.tsx | 43 - .../toolpad-core/src/PageContainer/index.ts | 3 - .../src/SignInPage/SignInPage.test.tsx | 85 -- .../src/SignInPage/SignInPage.tsx | 805 ------------- .../src/SignInPage/icons/Auth0.tsx | 17 - .../src/SignInPage/icons/Cognito.tsx | 39 - .../src/SignInPage/icons/Discord.tsx | 24 - .../src/SignInPage/icons/Facebook.tsx | 22 - .../src/SignInPage/icons/FusionAuth.tsx | 73 -- .../src/SignInPage/icons/GitLab.tsx | 28 - .../src/SignInPage/icons/Google.tsx | 31 - .../src/SignInPage/icons/Instagram.tsx | 40 - .../src/SignInPage/icons/Keycloak.tsx | 23 - .../src/SignInPage/icons/Line.tsx | 30 - .../src/SignInPage/icons/LinkedIn.tsx | 28 - .../src/SignInPage/icons/MicrosoftEntra.tsx | 17 - .../src/SignInPage/icons/Okta.tsx | 22 - .../src/SignInPage/icons/Slack.tsx | 30 - .../src/SignInPage/icons/Spotify.tsx | 15 - .../src/SignInPage/icons/TikTok.tsx | 32 - .../src/SignInPage/icons/Twitch.tsx | 21 - .../src/SignInPage/icons/Twitter.tsx | 23 - packages/toolpad-core/src/SignInPage/index.ts | 1 - packages/toolpad-core/src/index.ts | 25 - packages/toolpad-core/src/internal/context.ts | 4 - packages/toolpad-core/src/internal/demo.tsx | 33 - packages/toolpad-core/src/internal/index.tsx | 4 - packages/toolpad-core/src/locales/en.tsx | 59 - .../src/locales/getLocalization.ts | 13 - packages/toolpad-core/src/locales/hiIN.tsx | 41 - .../src/nextjs/NextAppProvider.test.tsx | 54 - .../src/nextjs/NextAppProvider.tsx | 14 - .../src/nextjs/NextAppProviderApp.tsx | 44 - .../src/nextjs/NextAppProviderPages.tsx | 57 - packages/toolpad-core/src/nextjs/index.tsx | 1 - .../toolpad-core/src/persistence/codec.tsx | 81 -- .../toolpad-core/src/persistence/index.ts | 1 - .../src/persistence/useStorageState.test.tsx | 82 -- .../src/persistence/useStorageState.tsx | 237 ---- .../ReactRouterAppProvider.test.tsx | 22 - .../react-router/ReactRouterAppProvider.tsx | 43 - .../toolpad-core/src/react-router/index.tsx | 1 - packages/toolpad-core/src/shared/Link.tsx | 55 - packages/toolpad-core/src/shared/branding.ts | 7 - .../toolpad-core/src/shared/components.tsx | 44 - packages/toolpad-core/src/shared/context.ts | 33 - .../toolpad-core/src/shared/navigation.tsx | 154 --- .../toolpad-core/src/useActivePage/index.ts | 1 - .../src/useActivePage/useActivePage.ts | 60 - .../src/useDialogs/DialogsContext.tsx | 12 - .../src/useDialogs/DialogsProvider.test.tsx | 16 - .../src/useDialogs/DialogsProvider.tsx | 114 -- packages/toolpad-core/src/useDialogs/index.ts | 2 - .../src/useDialogs/useDialogs.test.tsx | 224 ---- .../src/useDialogs/useDialogs.tsx | 368 ------ .../src/useLocalStorageState/index.ts | 1 - .../useLocalStorageState.tsx | 16 - .../useNotifications/NotificationsContext.ts | 14 - .../NotificationsProvider.test.tsx | 19 - .../NotificationsProvider.tsx | 212 ---- .../src/useNotifications/index.ts | 2 - .../useNotifications.test.tsx | 40 - .../src/useNotifications/useNotifications.tsx | 71 -- .../src/useSearchParamState/index.ts | 0 packages/toolpad-core/src/useSession/index.ts | 1 - .../src/useSession/useSession.test.tsx | 45 - .../toolpad-core/src/useSession/useSession.ts | 11 - .../src/useSessionStorageState/index.tsx | 1 - .../useSessionStorageState.tsx | 16 - packages/toolpad-core/tsconfig.build.json | 12 - packages/toolpad-core/tsconfig.json | 24 - packages/toolpad-core/vitest.config.mts | 24 - .../toolpad-studio-components/package.json | 2 +- packages/toolpad-studio-runtime/package.json | 2 +- packages/toolpad-studio/package.json | 4 +- packages/toolpad-utils/README.md | 3 - packages/toolpad-utils/package.json | 61 - packages/toolpad-utils/src/cli.spec.ts | 11 - packages/toolpad-utils/src/cli.ts | 9 - .../toolpad-utils/src/collections.spec.ts | 26 - packages/toolpad-utils/src/collections.ts | 107 -- .../toolpad-utils/src/comparators.spec.ts | 60 - packages/toolpad-utils/src/comparators.ts | 25 - .../toolpad-utils/src/describeConformance.ts | 14 - packages/toolpad-utils/src/errors.spec.ts | 22 - packages/toolpad-utils/src/errors.ts | 58 - packages/toolpad-utils/src/events.spec.ts | 47 - packages/toolpad-utils/src/events.ts | 76 -- packages/toolpad-utils/src/fs.spec.ts | 60 - packages/toolpad-utils/src/fs.ts | 132 --- packages/toolpad-utils/src/hooks/index.ts | 2 - .../toolpad-utils/src/hooks/useBoolean.ts | 12 - .../toolpad-utils/src/hooks/useDebounced.ts | 36 - .../src/hooks/useDebouncedHandler.ts | 71 -- packages/toolpad-utils/src/hooks/useLatest.ts | 16 - .../toolpad-utils/src/hooks/usePageTitle.ts | 14 - packages/toolpad-utils/src/hooks/useSsr.ts | 20 - .../src/hooks/useStorageState.ts | 114 -- packages/toolpad-utils/src/http.ts | 37 - packages/toolpad-utils/src/httpApiAdapters.ts | 44 - packages/toolpad-utils/src/immutability.ts | 105 -- packages/toolpad-utils/src/json.ts | 44 - packages/toolpad-utils/src/objectKey.spec.ts | 15 - packages/toolpad-utils/src/objectKey.ts | 20 - packages/toolpad-utils/src/path.spec.ts | 13 - packages/toolpad-utils/src/path.ts | 12 - packages/toolpad-utils/src/promises.spec.ts | 20 - packages/toolpad-utils/src/promises.ts | 9 - packages/toolpad-utils/src/react.spec.tsx | 58 - packages/toolpad-utils/src/react.tsx | 125 -- packages/toolpad-utils/src/strings.spec.ts | 136 --- packages/toolpad-utils/src/strings.ts | 209 ---- packages/toolpad-utils/src/types.ts | 98 -- packages/toolpad-utils/src/warnOnce.ts | 12 - packages/toolpad-utils/src/workerRpc.ts | 114 -- packages/toolpad-utils/tsconfig.build.json | 12 - packages/toolpad-utils/tsconfig.json | 17 - packages/toolpad-utils/vitest.config.mts | 12 - playground/nextjs-pages/.eslintrc.json | 3 - playground/nextjs-pages/README.md | 34 - playground/nextjs-pages/next-env.d.ts | 6 - playground/nextjs-pages/next.config.mjs | 8 - playground/nextjs-pages/package.json | 25 - .../src/app/api/auth/[...nextauth]/route.ts | 3 - playground/nextjs-pages/src/auth.ts | 77 -- playground/nextjs-pages/src/data/orders.ts | 199 ---- playground/nextjs-pages/src/middleware.ts | 6 - playground/nextjs-pages/src/pages/_app.tsx | 129 -- .../nextjs-pages/src/pages/_document.tsx | 27 - .../nextjs-pages/src/pages/auth/signin.tsx | 90 -- playground/nextjs-pages/src/pages/index.tsx | 11 - .../src/pages/orders/[[...segments]].tsx | 15 - playground/nextjs-pages/tsconfig.json | 24 - playground/nextjs/.eslintrc.json | 3 - playground/nextjs/.gitignore | 5 - playground/nextjs/README.md | 34 - playground/nextjs/next-env.d.ts | 5 - playground/nextjs/next.config.mjs | 4 - playground/nextjs/package.json | 26 - .../nextjs/src/app/(dashboard)/layout.tsx | 183 --- .../orders/[[...segments]]/page.tsx | 15 - .../nextjs/src/app/(dashboard)/page.tsx | 9 - .../src/app/api/auth/[...nextauth]/route.ts | 3 - .../nextjs/src/app/auth/signin/actions.ts | 40 - .../nextjs/src/app/auth/signin/page.tsx | 27 - playground/nextjs/src/app/layout.tsx | 57 - playground/nextjs/src/app/public/layout.tsx | 11 - playground/nextjs/src/app/public/page.tsx | 6 - playground/nextjs/src/auth.ts | 77 -- playground/nextjs/src/data/orders.ts | 199 ---- playground/nextjs/src/middleware.ts | 6 - playground/nextjs/tsconfig.json | 24 - playground/vite/.gitignore | 24 - playground/vite/README.md | 8 - playground/vite/index.html | 20 - playground/vite/package.json | 25 - playground/vite/public/vite.svg | 1 - playground/vite/src/App.tsx | 35 - playground/vite/src/assets/.gitkeep | 0 playground/vite/src/data/orders.ts | 198 ---- playground/vite/src/layouts/dashboard.tsx | 30 - playground/vite/src/main.tsx | 35 - playground/vite/src/pages/index.tsx | 6 - playground/vite/src/pages/orders.tsx | 15 - playground/vite/src/vite-env.d.ts | 1 - playground/vite/tsconfig.json | 20 - playground/vite/vite.config.ts | 7 - pnpm-lock.yaml | 1046 ++--------------- test/package.json | 2 +- 271 files changed, 108 insertions(+), 18909 deletions(-) delete mode 100644 packages/create-toolpad-app/README.md delete mode 100644 packages/create-toolpad-app/package.json delete mode 100644 packages/create-toolpad-app/public/templates/gitignoreTemplate delete mode 100644 packages/create-toolpad-app/src/core.ts delete mode 100644 packages/create-toolpad-app/src/examples.ts delete mode 100644 packages/create-toolpad-app/src/generateProject.ts delete mode 100644 packages/create-toolpad-app/src/generateStudioProject.ts delete mode 100644 packages/create-toolpad-app/src/index.ts delete mode 100644 packages/create-toolpad-app/src/package.ts delete mode 100644 packages/create-toolpad-app/src/packageType.ts delete mode 100644 packages/create-toolpad-app/src/studio.ts delete mode 100644 packages/create-toolpad-app/src/templates/eslintConfig.ts delete mode 100644 packages/create-toolpad-app/src/templates/gitignore.ts delete mode 100644 packages/create-toolpad-app/src/templates/indexPage.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextConfig.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextTypes.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/env.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/envLocal.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/middleware.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/actions.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/prisma.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/route.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/schemaPrisma.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts delete mode 100644 packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/document.ts delete mode 100644 packages/create-toolpad-app/src/templates/ordersPage.ts delete mode 100644 packages/create-toolpad-app/src/templates/packageJson.ts delete mode 100644 packages/create-toolpad-app/src/templates/readme.ts delete mode 100644 packages/create-toolpad-app/src/templates/studio/packageJson.ts delete mode 100644 packages/create-toolpad-app/src/templates/theme.ts delete mode 100644 packages/create-toolpad-app/src/templates/tsConfig.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/App.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/SessionContext.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/auth/env.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/auth/firebase.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/auth/firebaseConfig.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/auth/signin.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/html.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/main.ts delete mode 100644 packages/create-toolpad-app/src/templates/vite/viteConfig.ts delete mode 100644 packages/create-toolpad-app/src/types.ts delete mode 100644 packages/create-toolpad-app/src/validation.ts delete mode 100644 packages/create-toolpad-app/src/writeFiles.ts delete mode 100644 packages/create-toolpad-app/tests/index.spec.ts delete mode 100644 packages/create-toolpad-app/tests/package.json delete mode 100644 packages/create-toolpad-app/tests/tsconfig.json delete mode 100644 packages/create-toolpad-app/tsconfig.json delete mode 100644 packages/create-toolpad-app/tsup.config.ts delete mode 100644 packages/create-toolpad-app/vitest.config.mts delete mode 100644 packages/toolpad-core/README.md delete mode 100644 packages/toolpad-core/package.json delete mode 100644 packages/toolpad-core/src/Account/Account.test.tsx delete mode 100644 packages/toolpad-core/src/Account/Account.tsx delete mode 100644 packages/toolpad-core/src/Account/AccountLocaleContext.tsx delete mode 100644 packages/toolpad-core/src/Account/AccountPopoverFooter.tsx delete mode 100644 packages/toolpad-core/src/Account/AccountPopoverHeader.tsx delete mode 100644 packages/toolpad-core/src/Account/AccountPreview.test.tsx delete mode 100644 packages/toolpad-core/src/Account/AccountPreview.tsx delete mode 100644 packages/toolpad-core/src/Account/SignInButton.tsx delete mode 100644 packages/toolpad-core/src/Account/SignOutButton.tsx delete mode 100644 packages/toolpad-core/src/Account/index.ts delete mode 100644 packages/toolpad-core/src/AppProvider/AppProvider.test.tsx delete mode 100644 packages/toolpad-core/src/AppProvider/AppProvider.tsx delete mode 100644 packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx delete mode 100644 packages/toolpad-core/src/AppProvider/LocalizationProvider.tsx delete mode 100644 packages/toolpad-core/src/AppProvider/index.ts delete mode 100644 packages/toolpad-core/src/Crud/Create.tsx delete mode 100644 packages/toolpad-core/src/Crud/Crud.test.tsx delete mode 100644 packages/toolpad-core/src/Crud/Crud.tsx delete mode 100644 packages/toolpad-core/src/Crud/CrudForm.tsx delete mode 100644 packages/toolpad-core/src/Crud/CrudProvider.tsx delete mode 100644 packages/toolpad-core/src/Crud/Edit.tsx delete mode 100644 packages/toolpad-core/src/Crud/List.tsx delete mode 100644 packages/toolpad-core/src/Crud/Show.tsx delete mode 100644 packages/toolpad-core/src/Crud/cache.ts delete mode 100644 packages/toolpad-core/src/Crud/index.ts delete mode 100644 packages/toolpad-core/src/Crud/localeText.ts delete mode 100644 packages/toolpad-core/src/Crud/types.ts delete mode 100644 packages/toolpad-core/src/Crud/useCachedDataSource.ts delete mode 100644 packages/toolpad-core/src/DashboardLayout/AppTitle.tsx delete mode 100644 packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx delete mode 100644 packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx delete mode 100644 packages/toolpad-core/src/DashboardLayout/DashboardSidebarSubNavigation.tsx delete mode 100644 packages/toolpad-core/src/DashboardLayout/ThemeSwitcher.tsx delete mode 100644 packages/toolpad-core/src/DashboardLayout/ToolbarActions.tsx delete mode 100644 packages/toolpad-core/src/DashboardLayout/ToolpadLogo.tsx delete mode 100644 packages/toolpad-core/src/DashboardLayout/index.ts delete mode 100644 packages/toolpad-core/src/DashboardLayout/shared.ts delete mode 100644 packages/toolpad-core/src/DashboardLayout/utils.ts delete mode 100644 packages/toolpad-core/src/PageContainer/PageContainer.test.tsx delete mode 100644 packages/toolpad-core/src/PageContainer/PageContainer.tsx delete mode 100644 packages/toolpad-core/src/PageContainer/PageHeader.tsx delete mode 100644 packages/toolpad-core/src/PageContainer/PageHeaderToolbar.test.tsx delete mode 100644 packages/toolpad-core/src/PageContainer/PageHeaderToolbar.tsx delete mode 100644 packages/toolpad-core/src/PageContainer/index.ts delete mode 100644 packages/toolpad-core/src/SignInPage/SignInPage.test.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/SignInPage.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Auth0.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Cognito.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Discord.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Facebook.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/FusionAuth.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/GitLab.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Google.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Instagram.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Keycloak.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Line.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/LinkedIn.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/MicrosoftEntra.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Okta.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Slack.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Spotify.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/TikTok.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Twitch.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/icons/Twitter.tsx delete mode 100644 packages/toolpad-core/src/SignInPage/index.ts delete mode 100644 packages/toolpad-core/src/index.ts delete mode 100644 packages/toolpad-core/src/internal/context.ts delete mode 100644 packages/toolpad-core/src/internal/demo.tsx delete mode 100644 packages/toolpad-core/src/internal/index.tsx delete mode 100644 packages/toolpad-core/src/locales/en.tsx delete mode 100644 packages/toolpad-core/src/locales/getLocalization.ts delete mode 100644 packages/toolpad-core/src/locales/hiIN.tsx delete mode 100644 packages/toolpad-core/src/nextjs/NextAppProvider.test.tsx delete mode 100644 packages/toolpad-core/src/nextjs/NextAppProvider.tsx delete mode 100644 packages/toolpad-core/src/nextjs/NextAppProviderApp.tsx delete mode 100644 packages/toolpad-core/src/nextjs/NextAppProviderPages.tsx delete mode 100644 packages/toolpad-core/src/nextjs/index.tsx delete mode 100644 packages/toolpad-core/src/persistence/codec.tsx delete mode 100644 packages/toolpad-core/src/persistence/index.ts delete mode 100644 packages/toolpad-core/src/persistence/useStorageState.test.tsx delete mode 100644 packages/toolpad-core/src/persistence/useStorageState.tsx delete mode 100644 packages/toolpad-core/src/react-router/ReactRouterAppProvider.test.tsx delete mode 100644 packages/toolpad-core/src/react-router/ReactRouterAppProvider.tsx delete mode 100644 packages/toolpad-core/src/react-router/index.tsx delete mode 100644 packages/toolpad-core/src/shared/Link.tsx delete mode 100644 packages/toolpad-core/src/shared/branding.ts delete mode 100644 packages/toolpad-core/src/shared/components.tsx delete mode 100644 packages/toolpad-core/src/shared/context.ts delete mode 100644 packages/toolpad-core/src/shared/navigation.tsx delete mode 100644 packages/toolpad-core/src/useActivePage/index.ts delete mode 100644 packages/toolpad-core/src/useActivePage/useActivePage.ts delete mode 100644 packages/toolpad-core/src/useDialogs/DialogsContext.tsx delete mode 100644 packages/toolpad-core/src/useDialogs/DialogsProvider.test.tsx delete mode 100644 packages/toolpad-core/src/useDialogs/DialogsProvider.tsx delete mode 100644 packages/toolpad-core/src/useDialogs/index.ts delete mode 100644 packages/toolpad-core/src/useDialogs/useDialogs.test.tsx delete mode 100644 packages/toolpad-core/src/useDialogs/useDialogs.tsx delete mode 100644 packages/toolpad-core/src/useLocalStorageState/index.ts delete mode 100644 packages/toolpad-core/src/useLocalStorageState/useLocalStorageState.tsx delete mode 100644 packages/toolpad-core/src/useNotifications/NotificationsContext.ts delete mode 100644 packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx delete mode 100644 packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx delete mode 100644 packages/toolpad-core/src/useNotifications/index.ts delete mode 100644 packages/toolpad-core/src/useNotifications/useNotifications.test.tsx delete mode 100644 packages/toolpad-core/src/useNotifications/useNotifications.tsx delete mode 100644 packages/toolpad-core/src/useSearchParamState/index.ts delete mode 100644 packages/toolpad-core/src/useSession/index.ts delete mode 100644 packages/toolpad-core/src/useSession/useSession.test.tsx delete mode 100644 packages/toolpad-core/src/useSession/useSession.ts delete mode 100644 packages/toolpad-core/src/useSessionStorageState/index.tsx delete mode 100644 packages/toolpad-core/src/useSessionStorageState/useSessionStorageState.tsx delete mode 100644 packages/toolpad-core/tsconfig.build.json delete mode 100644 packages/toolpad-core/tsconfig.json delete mode 100644 packages/toolpad-core/vitest.config.mts delete mode 100644 packages/toolpad-utils/README.md delete mode 100644 packages/toolpad-utils/package.json delete mode 100644 packages/toolpad-utils/src/cli.spec.ts delete mode 100644 packages/toolpad-utils/src/cli.ts delete mode 100644 packages/toolpad-utils/src/collections.spec.ts delete mode 100644 packages/toolpad-utils/src/collections.ts delete mode 100644 packages/toolpad-utils/src/comparators.spec.ts delete mode 100644 packages/toolpad-utils/src/comparators.ts delete mode 100644 packages/toolpad-utils/src/describeConformance.ts delete mode 100644 packages/toolpad-utils/src/errors.spec.ts delete mode 100644 packages/toolpad-utils/src/errors.ts delete mode 100644 packages/toolpad-utils/src/events.spec.ts delete mode 100644 packages/toolpad-utils/src/events.ts delete mode 100644 packages/toolpad-utils/src/fs.spec.ts delete mode 100644 packages/toolpad-utils/src/fs.ts delete mode 100644 packages/toolpad-utils/src/hooks/index.ts delete mode 100644 packages/toolpad-utils/src/hooks/useBoolean.ts delete mode 100644 packages/toolpad-utils/src/hooks/useDebounced.ts delete mode 100644 packages/toolpad-utils/src/hooks/useDebouncedHandler.ts delete mode 100644 packages/toolpad-utils/src/hooks/useLatest.ts delete mode 100644 packages/toolpad-utils/src/hooks/usePageTitle.ts delete mode 100644 packages/toolpad-utils/src/hooks/useSsr.ts delete mode 100644 packages/toolpad-utils/src/hooks/useStorageState.ts delete mode 100644 packages/toolpad-utils/src/http.ts delete mode 100644 packages/toolpad-utils/src/httpApiAdapters.ts delete mode 100644 packages/toolpad-utils/src/immutability.ts delete mode 100644 packages/toolpad-utils/src/json.ts delete mode 100644 packages/toolpad-utils/src/objectKey.spec.ts delete mode 100644 packages/toolpad-utils/src/objectKey.ts delete mode 100644 packages/toolpad-utils/src/path.spec.ts delete mode 100644 packages/toolpad-utils/src/path.ts delete mode 100644 packages/toolpad-utils/src/promises.spec.ts delete mode 100644 packages/toolpad-utils/src/promises.ts delete mode 100644 packages/toolpad-utils/src/react.spec.tsx delete mode 100644 packages/toolpad-utils/src/react.tsx delete mode 100644 packages/toolpad-utils/src/strings.spec.ts delete mode 100644 packages/toolpad-utils/src/strings.ts delete mode 100644 packages/toolpad-utils/src/types.ts delete mode 100644 packages/toolpad-utils/src/warnOnce.ts delete mode 100644 packages/toolpad-utils/src/workerRpc.ts delete mode 100644 packages/toolpad-utils/tsconfig.build.json delete mode 100644 packages/toolpad-utils/tsconfig.json delete mode 100644 packages/toolpad-utils/vitest.config.mts delete mode 100644 playground/nextjs-pages/.eslintrc.json delete mode 100644 playground/nextjs-pages/README.md delete mode 100644 playground/nextjs-pages/next-env.d.ts delete mode 100644 playground/nextjs-pages/next.config.mjs delete mode 100644 playground/nextjs-pages/package.json delete mode 100644 playground/nextjs-pages/src/app/api/auth/[...nextauth]/route.ts delete mode 100644 playground/nextjs-pages/src/auth.ts delete mode 100644 playground/nextjs-pages/src/data/orders.ts delete mode 100644 playground/nextjs-pages/src/middleware.ts delete mode 100644 playground/nextjs-pages/src/pages/_app.tsx delete mode 100644 playground/nextjs-pages/src/pages/_document.tsx delete mode 100644 playground/nextjs-pages/src/pages/auth/signin.tsx delete mode 100644 playground/nextjs-pages/src/pages/index.tsx delete mode 100644 playground/nextjs-pages/src/pages/orders/[[...segments]].tsx delete mode 100644 playground/nextjs-pages/tsconfig.json delete mode 100644 playground/nextjs/.eslintrc.json delete mode 100644 playground/nextjs/.gitignore delete mode 100644 playground/nextjs/README.md delete mode 100644 playground/nextjs/next-env.d.ts delete mode 100644 playground/nextjs/next.config.mjs delete mode 100644 playground/nextjs/package.json delete mode 100644 playground/nextjs/src/app/(dashboard)/layout.tsx delete mode 100644 playground/nextjs/src/app/(dashboard)/orders/[[...segments]]/page.tsx delete mode 100644 playground/nextjs/src/app/(dashboard)/page.tsx delete mode 100644 playground/nextjs/src/app/api/auth/[...nextauth]/route.ts delete mode 100644 playground/nextjs/src/app/auth/signin/actions.ts delete mode 100644 playground/nextjs/src/app/auth/signin/page.tsx delete mode 100644 playground/nextjs/src/app/layout.tsx delete mode 100644 playground/nextjs/src/app/public/layout.tsx delete mode 100644 playground/nextjs/src/app/public/page.tsx delete mode 100644 playground/nextjs/src/auth.ts delete mode 100644 playground/nextjs/src/data/orders.ts delete mode 100644 playground/nextjs/src/middleware.ts delete mode 100644 playground/nextjs/tsconfig.json delete mode 100644 playground/vite/.gitignore delete mode 100644 playground/vite/README.md delete mode 100644 playground/vite/index.html delete mode 100644 playground/vite/package.json delete mode 100644 playground/vite/public/vite.svg delete mode 100644 playground/vite/src/App.tsx delete mode 100644 playground/vite/src/assets/.gitkeep delete mode 100644 playground/vite/src/data/orders.ts delete mode 100644 playground/vite/src/layouts/dashboard.tsx delete mode 100644 playground/vite/src/main.tsx delete mode 100644 playground/vite/src/pages/index.tsx delete mode 100644 playground/vite/src/pages/orders.tsx delete mode 100644 playground/vite/src/vite-env.d.ts delete mode 100644 playground/vite/tsconfig.json delete mode 100644 playground/vite/vite.config.ts diff --git a/.codesandbox/ci.json b/.codesandbox/ci.json index b3022147b8d..a02dcecfcc3 100644 --- a/.codesandbox/ci.json +++ b/.codesandbox/ci.json @@ -3,20 +3,14 @@ "buildCommand": "release:build", "node": "20", "packages": [ - "packages/create-toolpad-app", - "packages/toolpad-core", "packages/toolpad-studio", "packages/toolpad-studio-components", - "packages/toolpad-studio-runtime", - "packages/toolpad-utils" + "packages/toolpad-studio-runtime" ], "publishDirectory": { - "create-toolpad-app": "packages/create-toolpad-app", - "@toolpad/core": "packages/toolpad-core/build", "@toolpad/studio": "packages/toolpad-studio", "@toolpad/studio-components": "packages/toolpad-studio-components", - "@toolpad/studio-runtime": "packages/toolpad-studio-runtime", - "@toolpad/utils": "packages/toolpad-utils/build" + "@toolpad/studio-runtime": "packages/toolpad-studio-runtime" }, "silent": true } diff --git a/docs/package.json b/docs/package.json index e610ea5393e..898d3307518 100644 --- a/docs/package.json +++ b/docs/package.json @@ -39,7 +39,7 @@ "@mui/x-date-pickers": "7.28.0", "@mui/x-date-pickers-pro": "7.28.0", "@mui/x-license": "7.28.0", - "@toolpad/core": "workspace:*", + "@toolpad/core": "0.13.0", "@toolpad/studio": "workspace:*", "@trendmicro/react-interpolate": "0.5.5", "@types/lodash": "4.17.16", diff --git a/packages/create-toolpad-app/README.md b/packages/create-toolpad-app/README.md deleted file mode 100644 index 95155312274..00000000000 --- a/packages/create-toolpad-app/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# Create Toolpad App - -The simplest method to start with Toolpad is by using `create-toolpad-app`. -This CLI tool enables you to quickly start building a new Toolpad Core application, with everything already set up. You can use the `--studio` flag if you want to bootstrap a Toolpad Studio project. - -## Interactive - -You can create a new project interactively by running: - -```bash -npx create-toolpad-app@latest -# or -yarn create toolpad-app -# or -pnpm create toolpad-app -``` - -You then need to provide the name of your project: - -```bash -✔ Enter the name of your project: -``` - -Enter the name to install the necessary types/dependencies and create a new Toolpad Core project. diff --git a/packages/create-toolpad-app/package.json b/packages/create-toolpad-app/package.json deleted file mode 100644 index f0ea4046e8b..00000000000 --- a/packages/create-toolpad-app/package.json +++ /dev/null @@ -1,52 +0,0 @@ -{ - "name": "create-toolpad-app", - "version": "0.13.0", - "keywords": [ - "react", - "toolpad", - "mui" - ], - "description": "Create Toolpad apps with one command", - "repository": { - "type": "git", - "url": "git+https://github.com/mui/toolpad.git", - "directory": "packages/create-toolpad-app" - }, - "author": "Toolpad Team", - "license": "MIT", - "bin": { - "create-toolpad-app": "./dist/index.js" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "tsup", - "dev": "tsup --watch", - "check-types": "tsc --noEmit", - "test": "vitest run" - }, - "engines": { - "node": ">=18" - }, - "dependencies": { - "@inquirer/prompts": "^6.0.1", - "@toolpad/core": "workspace:*", - "@toolpad/utils": "workspace:*", - "chalk": "5.4.1", - "execa": "9.5.2", - "invariant": "2.2.4", - "semver": "7.6.3", - "tar": "7.4.3", - "yargs": "17.7.2" - }, - "devDependencies": { - "@types/inquirer": "9.0.7", - "@types/invariant": "2.2.37", - "@types/node": "^20.17.24", - "@types/semver": "7.5.8", - "@types/tar": "6.1.13", - "@types/yargs": "17.0.33", - "terminate": "^2.8.0" - } -} diff --git a/packages/create-toolpad-app/public/templates/gitignoreTemplate b/packages/create-toolpad-app/public/templates/gitignoreTemplate deleted file mode 100644 index fb4775ed548..00000000000 --- a/packages/create-toolpad-app/public/templates/gitignoreTemplate +++ /dev/null @@ -1,166 +0,0 @@ -# Logs - -logs -_.log -npm-debug.log_ -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) - -report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json - -# Runtime data - -pids -_.pid -_.seed -\*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover - -lib-cov - -# Coverage directory used by tools like istanbul - -coverage -\*.lcov - -# nyc test coverage - -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) - -.grunt - -# Bower dependency directory (https://bower.io/) - -bower_components - -# node-waf configuration - -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) - -build/Release - -# Dependency directories - -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) - -web_modules/ - -# TypeScript cache - -\*.tsbuildinfo - -# Optional npm cache directory - -.npm - -# Optional eslint cache - -.eslintcache - -# Optional stylelint cache - -.stylelintcache - -# Microbundle cache - -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history - -.node_repl_history - -# Output of 'npm pack' - -\*.tgz - -# Yarn Integrity file - -.yarn-integrity - -# dotenv environment variable files - -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) - -.cache -.parcel-cache - -# Next.js build output - -.next -out - -# Nuxt.js build / generate output - -.nuxt -dist - -# Gatsby files - -.cache/ - -# Comment in the public line in if your project uses Gatsby and not Next.js - -# https://nextjs.org/blog/next-9-1#public-directory-support - -# public - -# vuepress build output - -.vuepress/dist - -# vuepress v2.x temp and cache directory - -.temp -.cache - -# Docusaurus cache and generated files - -.docusaurus - -# Serverless directories - -.serverless/ - -# FuseBox cache - -.fusebox/ - -# DynamoDB Local files - -.dynamodb/ - -# TernJS port file - -.tern-port - -# Stores VS Code versions used for testing VS Code extensions - -.vscode-test - -# yarn v2 - -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz diff --git a/packages/create-toolpad-app/src/core.ts b/packages/create-toolpad-app/src/core.ts deleted file mode 100644 index 54bed93df00..00000000000 --- a/packages/create-toolpad-app/src/core.ts +++ /dev/null @@ -1,52 +0,0 @@ -import chalk from 'chalk'; -import { execa } from 'execa'; -import type { GenerateProjectOptions } from './types'; -import generateProject from './generateProject'; -import writeFiles from './writeFiles'; - -export async function scaffoldCoreProject(options: GenerateProjectOptions): Promise { - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log( - `${chalk.cyan('info')} - Creating Toolpad Core project in ${chalk.cyan(options.absolutePath)}`, - ); - // eslint-disable-next-line no-console - console.log(); - - const packageManager = options.packageManager; - - const files = generateProject(options); - await writeFiles(options.absolutePath, files); - - if (options.install) { - // eslint-disable-next-line no-console - console.log(`${chalk.cyan('info')} - Installing dependencies`); - // eslint-disable-next-line no-console - console.log(); - - await execa(packageManager, ['install'], { - stdio: 'inherit', - cwd: options.absolutePath, - }); - - // eslint-disable-next-line no-console - console.log(); - } - - // eslint-disable-next-line no-console - console.log( - `${chalk.green('success')} - Created Toolpad Core project at ${chalk.cyan(options.absolutePath)}`, - ); - // eslint-disable-next-line no-console - console.log(); - - if (options.auth) { - // eslint-disable-next-line no-console - console.log( - `${chalk.cyan('info')} - Bootstrapped ${chalk.cyan('env.local')} with empty values. See https://authjs.dev/getting-started on how to add your credentials.`, - ); - // eslint-disable-next-line no-console - console.log(); - } -} diff --git a/packages/create-toolpad-app/src/examples.ts b/packages/create-toolpad-app/src/examples.ts deleted file mode 100644 index af0c24f1ca3..00000000000 --- a/packages/create-toolpad-app/src/examples.ts +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Loosely adapted from - * https://github.com/vercel/next.js/blob/e2f3059b48a779c2a755c21da26570d251305c01/packages/create-next-app/helpers/examples.ts - */ -import * as path from 'path'; -import * as os from 'os'; -import chalk from 'chalk'; -import * as fs from 'fs/promises'; -import { createWriteStream } from 'fs'; -import * as tar from 'tar'; -import { Readable } from 'stream'; -import { pipeline } from 'stream/promises'; -import invariant from 'invariant'; - -async function downloadTar(url: string) { - const tempFile = path.join(os.tmpdir(), `toolpad-cta-example.temp-${Date.now()}`); - const response = await fetch(url); - - if (!response.ok) { - throw new Error(`HTTP ${response.status} - ${response.statusText}`); - } - invariant(response.body, 'Missing response body'); - - let current = 0; - const readable = Readable.fromWeb(response.body); - readable.on('data', (chunk) => { - process.stdout.write( - `Downloading… ${new Intl.NumberFormat('en-US', { style: 'unit', unit: 'megabyte' }).format(current / 1000000)}\r`, - ); - current += chunk.length; - }); - await pipeline(readable, createWriteStream(tempFile)); - - return tempFile; -} - -export async function downloadAndExtractExample(root: string, name: string) { - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log(`${chalk.cyan('info')} - Downloading example "${name}" to ${chalk.cyan(root)}`); - // eslint-disable-next-line no-console - console.log(); - const tempFile = await downloadTar('https://codeload.github.com/mui/toolpad/tar.gz/master'); - - await tar.x({ - file: tempFile, - cwd: root, - strip: 2 + name.split('/').length, - filter: (p) => - p.includes(`toolpad-master/examples/studio/${name}/`) || - p.includes(`toolpad-master/examples/core/${name}/`), - }); - - // eslint-disable-next-line no-console - console.log( - `${chalk.green('success')} - Downloaded and extracted "${name}" to ${chalk.cyan(root)}`, - ); - // eslint-disable-next-line no-console - console.log(); - - await fs.unlink(tempFile); -} diff --git a/packages/create-toolpad-app/src/generateProject.ts b/packages/create-toolpad-app/src/generateProject.ts deleted file mode 100644 index 82f9b3d9cb1..00000000000 --- a/packages/create-toolpad-app/src/generateProject.ts +++ /dev/null @@ -1,195 +0,0 @@ -// Common files for all apps -import theme from './templates/theme'; -import eslintConfig from './templates/eslintConfig'; -import readme from './templates/readme'; -import gitignore from './templates/gitignore'; -import ordersPage from './templates/ordersPage'; -import packageJson from './templates/packageJson'; -import indexPage from './templates/indexPage'; - -// Vite specific files -import viteApp from './templates/vite/App'; -import viteConfig from './templates/vite/viteConfig'; -import viteMain from './templates/vite/main'; -import viteHtml from './templates/vite/html'; -import viteDashboardLayout from './templates/vite/dashboardLayout'; -// Vite Auth specific files -import viteSessionContext from './templates/vite/SessionContext'; -import viteSignIn from './templates/vite/auth/signin'; -import viteEnv from './templates/vite/auth/env'; -import viteFirebaseAuth from './templates/vite/auth/firebase'; -import viteFirebaseConfig from './templates/vite/auth/firebaseConfig'; - -// Nextjs specific files -import tsConfig from './templates/tsConfig'; -import nextConfig from './templates/nextConfig'; -import nextTypes from './templates/nextTypes'; -// App router specific files -import rootLayout from './templates/nextjs/nextjs-app/rootLayout'; -import dashboardLayout from './templates/nextjs/nextjs-app/dashboardLayout'; - -// Pages router specific files -import app from './templates/nextjs/nextjs-pages/app'; -import document from './templates/nextjs/nextjs-pages/document'; - -// Auth specific files for all apps -import auth from './templates/nextjs/auth/auth'; -import envLocal from './templates/nextjs/auth/envLocal'; -import middleware from './templates/nextjs/auth/middleware'; -import routeHandler from './templates/nextjs/auth/route'; -import prisma from './templates/nextjs/auth/prisma'; -import env from './templates/nextjs/auth/env'; -import schemaPrisma from './templates/nextjs/auth/schemaPrisma'; - -// Auth files for app router -import signInPage from './templates/nextjs/auth/nextjs-app/signInPage'; -import signInAction from './templates/nextjs/auth/nextjs-app/actions'; - -// Auth files for pages router -import signInPagePagesRouter from './templates/nextjs/auth/nextjs-pages/signIn'; - -import { GenerateProjectOptions } from './types'; - -export default function generateProject( - options: GenerateProjectOptions, -): Map { - // Common files regardless of framework - const commonFiles = new Map([ - ['theme.ts', { content: theme }], - ['.eslintrc.json', { content: eslintConfig }], - ['README.md', { content: readme }], - ['.gitignore', { content: gitignore }], - [ - 'package.json', - { - content: JSON.stringify(packageJson(options), null, 2), - }, - ], - ]); - - switch (options.framework) { - case 'vite': { - const viteFiles = new Map([ - ['vite.config.mts', { content: viteConfig }], - ['src/main.tsx', { content: viteMain(options) }], - ['src/layouts/dashboard.tsx', { content: viteDashboardLayout(options) }], - ['src/App.tsx', { content: viteApp(options) }], - ['src/pages/index.tsx', { content: indexPage(options) }], - ['src/pages/orders.tsx', { content: ordersPage(options) }], - ['index.html', { content: viteHtml }], - ]); - - if (options.auth) { - const authFiles = new Map([ - ['src/firebase/auth.ts', { content: viteFirebaseAuth(options) }], - ['src/firebase/firebaseConfig.ts', { content: viteFirebaseConfig(options) }], - ['src/SessionContext.ts', { content: viteSessionContext }], - ['.env', { content: viteEnv }], - ['src/pages/signin.tsx', { content: viteSignIn(options) }], - ]); - - return new Map([...commonFiles, ...viteFiles, ...authFiles]); - } - - return new Map([...commonFiles, ...viteFiles]); - } - - case 'nextjs': - default: { - const nextCommonFiles = new Map([ - ['tsconfig.json', { content: tsConfig }], - ['next-env.d.ts', { content: nextTypes }], - ['next.config.mjs', { content: nextConfig(options) }], - ]); - - switch (options.router) { - case 'nextjs-pages': { - const nextJsPagesRouterStarter = new Map([ - ['pages/index.tsx', { content: indexPage(options) }], - ['pages/orders/index.tsx', { content: ordersPage(options) }], - ['pages/_document.tsx', { content: document }], - ['pages/_app.tsx', { content: app(options) }], - ]); - if (options.auth) { - const authFiles = new Map([ - ['auth.ts', { content: auth(options) }], - ['.env.local', { content: envLocal(options) }], - ['middleware.ts', { content: middleware }], - // next-auth v5 does not provide an API route, so this file must be in the app router - // even if the rest of the app is using pages router - // https://authjs.dev/getting-started/installation#configure - ['app/api/auth/[...nextAuth]/route.ts', { content: routeHandler }], - ['pages/auth/signin.tsx', { content: signInPagePagesRouter(options) }], - ]); - if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - // Prisma adapter support requires removal of middleware - authFiles.delete('middleware.ts'); - const prismaFiles = new Map([ - ['prisma.ts', { content: prisma }], - ['.env', { content: env }], - ['prisma/schema.prisma', { content: schemaPrisma(options) }], - ]); - return new Map([ - ...commonFiles, - ...nextCommonFiles, - ...nextJsPagesRouterStarter, - ...authFiles, - ...prismaFiles, - ]); - } - return new Map([ - ...commonFiles, - ...nextCommonFiles, - ...nextJsPagesRouterStarter, - ...authFiles, - ]); - } - return new Map([...commonFiles, ...nextCommonFiles, ...nextJsPagesRouterStarter]); - } - case 'nextjs-app': - default: { - const nextJsAppRouterStarter = new Map([ - ['app/(dashboard)/layout.tsx', { content: dashboardLayout }], - ['app/layout.tsx', { content: rootLayout(options) }], - ['app/(dashboard)/page.tsx', { content: indexPage(options) }], - ['app/(dashboard)/orders/page.tsx', { content: ordersPage(options) }], - ]); - if (options.auth) { - const authFiles = new Map([ - ['auth.ts', { content: auth(options) }], - ['.env.local', { content: envLocal(options) }], - ['middleware.ts', { content: middleware }], - ['app/api/auth/[...nextAuth]/route.ts', { content: routeHandler }], - ['app/auth/signin/page.tsx', { content: signInPage(options) }], - ['app/auth/signin/actions.ts', { content: signInAction(options) }], - ]); - if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - // Prisma adapater support requires removal of middleware - authFiles.delete('middleware.ts'); - const prismaFiles = new Map([ - ['prisma.ts', { content: prisma }], - ['.env', { content: env }], - ['prisma/schema.prisma', { content: schemaPrisma(options) }], - ]); - return new Map([ - ...commonFiles, - ...nextCommonFiles, - ...nextJsAppRouterStarter, - ...authFiles, - ...prismaFiles, - ]); - } - - return new Map([ - ...commonFiles, - ...nextCommonFiles, - ...nextJsAppRouterStarter, - ...authFiles, - ]); - } - return new Map([...commonFiles, ...nextCommonFiles, ...nextJsAppRouterStarter]); - } - } - } - } -} diff --git a/packages/create-toolpad-app/src/generateStudioProject.ts b/packages/create-toolpad-app/src/generateStudioProject.ts deleted file mode 100644 index 85e8d5ffb70..00000000000 --- a/packages/create-toolpad-app/src/generateStudioProject.ts +++ /dev/null @@ -1,14 +0,0 @@ -import packageJson from './templates/packageJson'; -import gitignore from './templates/gitignore'; -import type { GenerateProjectOptions } from './types'; - -export default function generateStudioProject( - options: GenerateProjectOptions, -): Map { - const files = new Map([ - ['package.json', { content: JSON.stringify(packageJson(options), null, 2) }], - ['.gitignore', { content: gitignore }], - ]); - - return files; -} diff --git a/packages/create-toolpad-app/src/index.ts b/packages/create-toolpad-app/src/index.ts deleted file mode 100644 index 9b7c6dc102c..00000000000 --- a/packages/create-toolpad-app/src/index.ts +++ /dev/null @@ -1,230 +0,0 @@ -#!/usr/bin/env node - -import path from 'path'; -import yargs from 'yargs'; -import { input, confirm, select, checkbox } from '@inquirer/prompts'; -import chalk from 'chalk'; -import { satisfies } from 'semver'; -import invariant from 'invariant'; -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; -import { bashResolvePath } from '@toolpad/utils/cli'; -import { downloadAndExtractExample } from './examples'; -import type { SupportedFramework, SupportedRouter, GenerateProjectOptions } from './types'; -import { findCtaPackageJson, getPackageManager } from './package'; -import { scaffoldCoreProject } from './core'; -import { scaffoldStudioProject } from './studio'; -import { validatePath } from './validation'; - -declare global { - interface Error { - code?: unknown; - } -} - -const run = async () => { - const pkgJson = await findCtaPackageJson(); - const packageManager = getPackageManager(); - - invariant(pkgJson.engines?.node, 'Missing node version in package.json'); - - if (!satisfies(process.version, pkgJson.engines.node)) { - // eslint-disable-next-line no-console - console.log( - `${chalk.red('error')} - Your node version ${ - process.version - } is unsupported. Please upgrade it to at least ${pkgJson.engines.node}`, - ); - process.exit(1); - } - - const args = await yargs(process.argv.slice(2)) - .scriptName('create-toolpad-app') - .usage('$0 [path] [options]') - .positional('path', { - type: 'string', - describe: 'The path where the Toolpad project directory will be created', - }) - .option('studio', { - type: 'boolean', - describe: 'Create a new project with Toolpad Studio', - default: false, - }) - .option('install', { - type: 'boolean', - describe: 'Install dependencies', - default: true, - }) - .option('core-version', { - type: 'string', - describe: 'Use a specific version of Toolpad Core', - }) - .option('example', { - type: 'string', - describe: - 'The name of one of the available examples. See https://github.com/mui/toolpad/tree/master/examples.', - }) - .help().argv; - - const pathArg = args._?.[0] as string; - const installFlag = args.install as boolean; - const studioFlag = args.studio as boolean; - const example = args.example as string; - - if (pathArg) { - if (!example) { - const pathValidOrError = await validatePath(pathArg, true); - if (typeof pathValidOrError === 'string') { - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log(pathValidOrError); - // eslint-disable-next-line no-console - console.log(); - process.exit(1); - } - } else { - await validatePath(pathArg); - } - } - - let projectPath = pathArg; - if (!pathArg) { - projectPath = await input({ - message: example - ? `Enter path of directory to download example "${chalk.cyan(example)}" into` - : 'Enter path of directory to bootstrap new app', - validate: (pathInput) => validatePath(pathInput, !example), - default: '.', - }); - } - - const absolutePath = bashResolvePath(projectPath); - - let hasNodemailerProvider = false; - let hasPasskeyProvider = false; - - if (example) { - await downloadAndExtractExample(absolutePath, example); - } else if (studioFlag) { - await scaffoldStudioProject(absolutePath, installFlag, packageManager); - } else { - const frameworkOption: SupportedFramework = await select({ - message: 'Which framework would you like to use?', - default: 'nextjs', - choices: [ - { name: 'Next.js', value: 'nextjs' }, - { name: 'Vite', value: 'vite' }, - ], - }); - - let routerOption: SupportedRouter | undefined; - - if (frameworkOption === 'nextjs') { - routerOption = await select({ - message: 'Which router would you like to use?', - default: 'nextjs-app', - choices: [ - { name: 'Next.js App Router', value: 'nextjs-app' }, - { name: 'Next.js Pages Router', value: 'nextjs-pages' }, - ], - }); - } - - const authFlag = await confirm({ - message: 'Would you like to enable authentication?', - default: true, - }); - let authProviderOptions: SupportedAuthProvider[] = []; - if (authFlag) { - authProviderOptions = await checkbox({ - message: 'Select authentication providers to enable:', - required: true, - choices: - frameworkOption === 'nextjs' - ? [ - { name: 'Google', value: 'google' }, - { name: 'GitHub', value: 'github' }, - { name: 'Passkey', value: 'passkey' }, - { name: 'Magic Link', value: 'nodemailer' }, - { name: 'Credentials', value: 'credentials' }, - { name: 'GitLab', value: 'gitlab' }, - { name: 'Twitter', value: 'twitter' }, - { name: 'Facebook', value: 'facebook' }, - { name: 'Cognito', value: 'cognito' }, - { name: 'Microsoft Entra ID', value: 'microsoft-entra-id' }, - { name: 'Apple', value: 'apple' }, - { name: 'Instagram', value: 'instagram' }, - { name: 'TikTok', value: 'tiktok' }, - { name: 'LinkedIn', value: 'linkedin' }, - { name: 'Slack', value: 'slack' }, - { name: 'Spotify', value: 'spotify' }, - { name: 'Twitch', value: 'twitch' }, - { name: 'Discord', value: 'discord' }, - { name: 'Line', value: 'line' }, - { name: 'Auth0', value: 'auth0' }, - { name: 'Keycloak', value: 'keycloak' }, - { name: 'Okta', value: 'okta' }, - { name: 'FusionAuth', value: 'fusionauth' }, - ] - : [ - { name: 'Google', value: 'google' }, - { name: 'GitHub', value: 'github' }, - { name: 'Credentials', value: 'credentials' }, - ], - }); - hasNodemailerProvider = authProviderOptions?.includes('nodemailer'); - hasPasskeyProvider = authProviderOptions?.includes('passkey'); - } - - const options: GenerateProjectOptions = { - name: path.basename(absolutePath), - absolutePath, - coreVersion: args.coreVersion ?? pkgJson.version, - router: routerOption, - framework: frameworkOption, - auth: authFlag, - install: installFlag, - authProviders: authProviderOptions, - hasCredentialsProvider: authProviderOptions?.includes('credentials'), - hasNodemailerProvider, - hasPasskeyProvider, - packageManager, - }; - - await scaffoldCoreProject(options); - } - - let changeDirectoryInstruction = ''; - if (example) { - if (!path.relative(process.cwd(), absolutePath)) { - changeDirectoryInstruction = ` cd ./${example}\n`; - } else { - changeDirectoryInstruction = ` cd ${path.basename(absolutePath)}/${example}\n`; - } - } else if (path.relative(process.cwd(), absolutePath)) { - changeDirectoryInstruction = ` cd ${path.relative(process.cwd(), absolutePath)}\n`; - } - - const installInstruction = example || !installFlag ? ` ${packageManager} install\n` : ''; - - const databaseInstruction = - hasNodemailerProvider || hasPasskeyProvider - ? ` npx prisma migrate dev --schema=prisma/schema.prisma\n` - : ''; - - const message = `Run the following to get started: \n\n${chalk.magentaBright( - `${changeDirectoryInstruction}${databaseInstruction}${installInstruction} ${packageManager}${ - packageManager === 'yarn' ? '' : ' run' - } dev`, - )}`; - - // eslint-disable-next-line no-console - console.log(message); - // eslint-disable-next-line no-console - console.log(); -}; - -run().catch((error) => { - console.error(error.message); - process.exit(1); -}); diff --git a/packages/create-toolpad-app/src/package.ts b/packages/create-toolpad-app/src/package.ts deleted file mode 100644 index e13a5855ece..00000000000 --- a/packages/create-toolpad-app/src/package.ts +++ /dev/null @@ -1,28 +0,0 @@ -import path from 'path'; -import * as fs from 'fs/promises'; -import type { PackageManager } from './types'; -import type { PackageJson } from './packageType'; - -export function getPackageManager(): PackageManager { - const userAgent = process.env.npm_config_user_agent; - - if (userAgent) { - if (userAgent.startsWith('yarn')) { - return 'yarn'; - } - if (userAgent.startsWith('pnpm')) { - return 'pnpm'; - } - if (userAgent.startsWith('npm')) { - return 'npm'; - } - } - return 'pnpm'; -} - -export async function findCtaPackageJson(): Promise { - const ctaPackageJsonPath = path.resolve(__dirname, '../package.json'); - const content = await fs.readFile(ctaPackageJsonPath, 'utf8'); - const packageJson = JSON.parse(content); - return packageJson; -} diff --git a/packages/create-toolpad-app/src/packageType.ts b/packages/create-toolpad-app/src/packageType.ts deleted file mode 100644 index 33376f61b34..00000000000 --- a/packages/create-toolpad-app/src/packageType.ts +++ /dev/null @@ -1,667 +0,0 @@ -declare namespace PackageJsonTypes { - /** - A person who has been involved in creating or maintaining the package. - */ - export type Person = - | string - | { - name: string; - url?: string; - email?: string; - }; - - export type BugsLocation = - | string - | { - /** - The URL to the package's issue tracker. - */ - url?: string; - - /** - The email address to which issues should be reported. - */ - email?: string; - }; - - export type DirectoryLocations = { - [directoryType: string]: any; - - /** - Location for executable scripts. Sugar to generate entries in the `bin` property by walking the folder. - */ - bin?: string; - - /** - Location for Markdown files. - */ - doc?: string; - - /** - Location for example scripts. - */ - example?: string; - - /** - Location for the bulk of the library. - */ - lib?: string; - - /** - Location for man pages. Sugar to generate a `man` array by walking the folder. - */ - man?: string; - - /** - Location for test files. - */ - test?: string; - }; - - export type Scripts = { - /** - Run **before** the package is published (Also run on local `npm install` without any arguments). - */ - prepublish?: string; - - /** - Run both **before** the package is packed and published, and on local `npm install` without any arguments. This is run **after** `prepublish`, but **before** `prepublishOnly`. - */ - prepare?: string; - - /** - Run **before** the package is prepared and packed, **only** on `npm publish`. - */ - prepublishOnly?: string; - - /** - Run **before** a tarball is packed (on `npm pack`, `npm publish`, and when installing git dependencies). - */ - prepack?: string; - - /** - Run **after** the tarball has been generated and moved to its final destination. - */ - postpack?: string; - - /** - Run **after** the package is published. - */ - publish?: string; - - /** - Run **after** the package is published. - */ - postpublish?: string; - - /** - Run **before** the package is installed. - */ - preinstall?: string; - - /** - Run **after** the package is installed. - */ - install?: string; - - /** - Run **after** the package is installed and after `install`. - */ - postinstall?: string; - - /** - Run **before** the package is uninstalled and before `uninstall`. - */ - preuninstall?: string; - - /** - Run **before** the package is uninstalled. - */ - uninstall?: string; - - /** - Run **after** the package is uninstalled. - */ - postuninstall?: string; - - /** - Run **before** bump the package version and before `version`. - */ - preversion?: string; - - /** - Run **before** bump the package version. - */ - version?: string; - - /** - Run **after** bump the package version. - */ - postversion?: string; - - /** - Run with the `npm test` command, before `test`. - */ - pretest?: string; - - /** - Run with the `npm test` command. - */ - test?: string; - - /** - Run with the `npm test` command, after `test`. - */ - posttest?: string; - - /** - Run with the `npm stop` command, before `stop`. - */ - prestop?: string; - - /** - Run with the `npm stop` command. - */ - stop?: string; - - /** - Run with the `npm stop` command, after `stop`. - */ - poststop?: string; - - /** - Run with the `npm start` command, before `start`. - */ - prestart?: string; - - /** - Run with the `npm start` command. - */ - start?: string; - - /** - Run with the `npm start` command, after `start`. - */ - poststart?: string; - - /** - Run with the `npm restart` command, before `restart`. Note: `npm restart` will run the `stop` and `start` scripts if no `restart` script is provided. - */ - prerestart?: string; - - /** - Run with the `npm restart` command. Note: `npm restart` will run the `stop` and `start` scripts if no `restart` script is provided. - */ - restart?: string; - - /** - Run with the `npm restart` command, after `restart`. Note: `npm restart` will run the `stop` and `start` scripts if no `restart` script is provided. - */ - postrestart?: string; - } & Partial>; - - /** - Dependencies of the package. The version range is a string which has one or more space-separated descriptors. Dependencies can also be identified with a tarball or Git URL. - */ - export type Dependency = Partial>; - - /** - A mapping of conditions and the paths to which they resolve. - */ - type ExportConditions = { - [condition: string]: Exports; - }; - - /** - Entry points of a module, optionally with conditions and subpath exports. - */ - export type Exports = null | string | Array | ExportConditions; - - /** - Import map entries of a module, optionally with conditions and subpath imports. - */ - export type Imports = { - [key: `#${string}`]: Exports; - }; - - export interface NonStandardEntryPoints { - /** - An ECMAScript module ID that is the primary entry point to the program. - */ - module?: string; - - /** - A module ID with untranspiled code that is the primary entry point to the program. - */ - esnext?: - | string - | { - [moduleName: string]: string | undefined; - main?: string; - browser?: string; - }; - - /** - A hint to JavaScript bundlers or component tools when packaging modules for client side use. - */ - browser?: string | Partial>; - - /** - Denote which files in your project are "pure" and therefore safe for Webpack to prune if unused. - - [Read more.](https://webpack.js.org/guides/tree-shaking/) - */ - sideEffects?: boolean | string[]; - } - - export type TypeScriptConfiguration = { - /** - Location of the bundled TypeScript declaration file. - */ - types?: string; - - /** - Version selection map of TypeScript. - */ - typesVersions?: Partial>>>; - - /** - Location of the bundled TypeScript declaration file. Alias of `types`. - */ - typings?: string; - }; - - /** - An alternative configuration for workspaces. - */ - export type WorkspaceConfig = { - /** - An array of workspace pattern strings which contain the workspace packages. - */ - packages?: WorkspacePattern[]; - - /** - Designed to solve the problem of packages which break when their `node_modules` are moved to the root workspace directory - a process known as hoisting. For these packages, both within your workspace, and also some that have been installed via `node_modules`, it is important to have a mechanism for preventing the default Yarn workspace behavior. By adding workspace pattern strings here, Yarn will resume non-workspace behavior for any package which matches the defined patterns. - - [Supported](https://classic.yarnpkg.com/blog/2018/02/15/nohoist/) by Yarn. - [Not supported](https://github.com/npm/rfcs/issues/287) by npm. - */ - nohoist?: WorkspacePattern[]; - }; - - /** - A workspace pattern points to a directory or group of directories which contain packages that should be included in the workspace installation process. - - The patterns are handled with [minimatch](https://github.com/isaacs/minimatch). - - @example - `docs` → Include the docs directory and install its dependencies. - `packages/*` → Include all nested directories within the packages directory, like `packages/cli` and `packages/core`. - */ - type WorkspacePattern = string; - - export type YarnConfiguration = { - /** - If your package only allows one version of a given dependency, and you'd like to enforce the same behavior as `yarn install --flat` on the command-line, set this to `true`. - - Note that if your `package.json` contains `"flat": true` and other packages depend on yours (for example you are building a library rather than an app), those other packages will also need `"flat": true` in their `package.json` or be installed with `yarn install --flat` on the command-line. - */ - flat?: boolean; - - /** - Selective version resolutions. Allows the definition of custom package versions inside dependencies without manual edits in the `yarn.lock` file. - */ - resolutions?: Dependency; - }; - - export type JSPMConfiguration = { - /** - JSPM configuration. - */ - jspm?: PackageJson; - }; - - /** - Type for [npm's `package.json` file](https://docs.npmjs.com/creating-a-package-json-file). Containing standard npm properties. - */ - export interface PackageJsonStandard { - /** - The name of the package. - */ - name?: string; - - /** - Package version, parseable by [`node-semver`](https://github.com/npm/node-semver). - */ - version?: string; - - /** - Package description, listed in `npm search`. - */ - description?: string; - - /** - Keywords associated with package, listed in `npm search`. - */ - keywords?: string[]; - - /** - The URL to the package's homepage. - */ - homepage?: LiteralUnion<'.', string>; - - /** - The URL to the package's issue tracker and/or the email address to which issues should be reported. - */ - bugs?: BugsLocation; - - /** - The license for the package. - */ - license?: string; - - /** - The licenses for the package. - */ - licenses?: Array<{ - type?: string; - url?: string; - }>; - - author?: Person; - - /** - A list of people who contributed to the package. - */ - contributors?: Person[]; - - /** - A list of people who maintain the package. - */ - maintainers?: Person[]; - - /** - The files included in the package. - */ - files?: string[]; - - /** - Resolution algorithm for importing ".js" files from the package's scope. - - [Read more.](https://nodejs.org/api/esm.html#esm_package_json_type_field) - */ - type?: 'module' | 'commonjs'; - - /** - The module ID that is the primary entry point to the program. - */ - main?: string; - - /** - Subpath exports to define entry points of the package. - - [Read more.](https://nodejs.org/api/packages.html#subpath-exports) - */ - exports?: Exports; - - /** - Subpath imports to define internal package import maps that only apply to import specifiers from within the package itself. - - [Read more.](https://nodejs.org/api/packages.html#subpath-imports) - */ - imports?: Imports; - - /** - The executable files that should be installed into the `PATH`. - */ - bin?: string | Partial>; - - /** - Filenames to put in place for the `man` program to find. - */ - man?: string | string[]; - - /** - Indicates the structure of the package. - */ - directories?: DirectoryLocations; - - /** - Location for the code repository. - */ - repository?: - | string - | { - type: string; - url: string; - - /** - Relative path to package.json if it is placed in non-root directory (for example if it is part of a monorepo). - - [Read more.](https://github.com/npm/rfcs/blob/latest/implemented/0010-monorepo-subdirectory-declaration.md) - */ - directory?: string; - }; - - /** - Script commands that are run at various times in the lifecycle of the package. The key is the lifecycle event, and the value is the command to run at that point. - */ - scripts?: Scripts; - - /** - Is used to set configuration parameters used in package scripts that persist across upgrades. - */ - config?: Record; - - /** - The dependencies of the package. - */ - dependencies?: Dependency; - - /** - Additional tooling dependencies that are not required for the package to work. Usually test, build, or documentation tooling. - */ - devDependencies?: Dependency; - - /** - Dependencies that are skipped if they fail to install. - */ - optionalDependencies?: Dependency; - - /** - Dependencies that will usually be required by the package user directly or via another dependency. - */ - peerDependencies?: Dependency; - - /** - Indicate peer dependencies that are optional. - */ - peerDependenciesMeta?: Partial>; - - /** - Package names that are bundled when the package is published. - */ - bundledDependencies?: string[]; - - /** - Alias of `bundledDependencies`. - */ - bundleDependencies?: string[]; - - /** - Engines that this package runs on. - */ - engines?: { - [EngineName in 'npm' | 'node' | string]?: string; - }; - - /** - @deprecated - */ - engineStrict?: boolean; - - /** - Operating systems the module runs on. - */ - os?: Array< - LiteralUnion< - | 'aix' - | 'darwin' - | 'freebsd' - | 'linux' - | 'openbsd' - | 'sunos' - | 'win32' - | '!aix' - | '!darwin' - | '!freebsd' - | '!linux' - | '!openbsd' - | '!sunos' - | '!win32', - string - > - >; - - /** - CPU architectures the module runs on. - */ - cpu?: Array< - LiteralUnion< - | 'arm' - | 'arm64' - | 'ia32' - | 'mips' - | 'mipsel' - | 'ppc' - | 'ppc64' - | 's390' - | 's390x' - | 'x32' - | 'x64' - | '!arm' - | '!arm64' - | '!ia32' - | '!mips' - | '!mipsel' - | '!ppc' - | '!ppc64' - | '!s390' - | '!s390x' - | '!x32' - | '!x64', - string - > - >; - - /** - If set to `true`, a warning will be shown if package is installed locally. Useful if the package is primarily a command-line application that should be installed globally. - - @deprecated - */ - preferGlobal?: boolean; - - /** - If set to `true`, then npm will refuse to publish it. - */ - private?: boolean; - - /** - A set of config values that will be used at publish-time. It's especially handy to set the tag, registry or access, to ensure that a given package is not tagged with 'latest', published to the global public registry or that a scoped module is private by default. - */ - publishConfig?: PublishConfig; - - /** - Describes and notifies consumers of a package's monetary support information. - - [Read more.](https://github.com/npm/rfcs/blob/latest/accepted/0017-add-funding-support.md) - */ - funding?: - | string - | { - /** - The type of funding. - */ - type?: LiteralUnion< - 'github' | 'opencollective' | 'patreon' | 'individual' | 'foundation' | 'corporation', - string - >; - - /** - The URL to the funding page. - */ - url: string; - }; - - /** - Used to configure [npm workspaces](https://docs.npmjs.com/cli/using-npm/workspaces) / [Yarn workspaces](https://classic.yarnpkg.com/docs/workspaces/). - - Workspaces allow you to manage multiple packages within the same repository in such a way that you only need to run your install command once in order to install all of them in a single pass. - - Please note that the top-level `private` property of `package.json` **must** be set to `true` in order to use workspaces. - */ - workspaces?: WorkspacePattern[] | WorkspaceConfig; - } - - /** - Type for [`package.json` file used by the Node.js runtime](https://nodejs.org/api/packages.html#nodejs-packagejson-field-definitions). - */ - export type NodeJsStandard = { - /** - Defines which package manager is expected to be used when working on the current project. It can set to any of the [supported package managers](https://nodejs.org/api/corepack.html#supported-package-managers), and will ensure that your teams use the exact same package manager versions without having to install anything else than Node.js. - - __This field is currently experimental and needs to be opted-in; check the [Corepack](https://nodejs.org/api/corepack.html) page for details about the procedure.__ - - @example - ```json - { - "packageManager": "@" - } - ``` - */ - packageManager?: string; - }; - - export type PublishConfig = { - /** - Additional, less common properties from the [npm docs on `publishConfig`](https://docs.npmjs.com/cli/v7/configuring-npm/package-json#publishconfig). - */ - [additionalProperties: string]: any; - - /** - When publishing scoped packages, the access level defaults to restricted. If you want your scoped package to be publicly viewable (and installable) set `--access=public`. The only valid values for access are public and restricted. Unscoped packages always have an access level of public. - */ - access?: 'public' | 'restricted'; - - /** - The base URL of the npm registry. - - Default: `'https://registry.npmjs.org/'` - */ - registry?: string; - - /** - The tag to publish the package under. - - Default: `'latest'` - */ - tag?: string; - }; -} - -type Primitive = null | undefined | string | number | boolean | symbol | bigint; - -type LiteralUnion = - | LiteralType - | (BaseType & Record); -/** -Type for [npm's `package.json` file](https://docs.npmjs.com/creating-a-package-json-file). Also includes types for fields used by other popular projects, like TypeScript and Yarn. - -@category File -*/ -export type PackageJson = PackageJsonTypes.NodeJsStandard & - PackageJsonTypes.PackageJsonStandard & - PackageJsonTypes.NonStandardEntryPoints & - PackageJsonTypes.TypeScriptConfiguration & - PackageJsonTypes.YarnConfiguration & - PackageJsonTypes.JSPMConfiguration; diff --git a/packages/create-toolpad-app/src/studio.ts b/packages/create-toolpad-app/src/studio.ts deleted file mode 100644 index 834c0ff0506..00000000000 --- a/packages/create-toolpad-app/src/studio.ts +++ /dev/null @@ -1,48 +0,0 @@ -import path from 'path'; -import chalk from 'chalk'; -import { execa } from 'execa'; - -import type { PackageManager, GenerateProjectOptions } from './types'; -import generateStudioProject from './generateStudioProject'; -import writeFiles from './writeFiles'; - -export async function scaffoldStudioProject( - absolutePath: string, - installFlag: boolean, - packageManager: PackageManager, -): Promise { - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log( - `${chalk.cyan('info')} - Creating Toolpad Studio project in ${chalk.cyan(absolutePath)}`, - ); - // eslint-disable-next-line no-console - console.log(); - - const options: GenerateProjectOptions = { - name: path.basename(absolutePath), - absolutePath, - projectType: 'studio', - packageManager, - }; - - const files = generateStudioProject(options); - await writeFiles(absolutePath, files); - - if (installFlag) { - // eslint-disable-next-line no-console - console.log(`${chalk.cyan('info')} - Installing dependencies`); - // eslint-disable-next-line no-console - console.log(); - - await execa(packageManager, ['install'], { stdio: 'inherit', cwd: absolutePath }); - - // eslint-disable-next-line no-console - console.log(); - // eslint-disable-next-line no-console - console.log(`${chalk.green('success')} - Dependencies installed successfully!`); - // eslint-disable-next-line no-console - console.log(); - } -} diff --git a/packages/create-toolpad-app/src/templates/eslintConfig.ts b/packages/create-toolpad-app/src/templates/eslintConfig.ts deleted file mode 100644 index fb9fc2e9548..00000000000 --- a/packages/create-toolpad-app/src/templates/eslintConfig.ts +++ /dev/null @@ -1,6 +0,0 @@ -const eslintConfig = `{ - "extends": "next/core-web-vitals" - } - `; - -export default eslintConfig; diff --git a/packages/create-toolpad-app/src/templates/gitignore.ts b/packages/create-toolpad-app/src/templates/gitignore.ts deleted file mode 100644 index 1cc389710b7..00000000000 --- a/packages/create-toolpad-app/src/templates/gitignore.ts +++ /dev/null @@ -1,149 +0,0 @@ -const gitignore = ` -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -lerna-debug.log* -.pnpm-debug.log* - -# Diagnostic reports (https://nodejs.org/api/report.html) -report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json - -# Runtime data -pids -*.pid -*.seed -*.pid.lock - -# Directory for instrumented libs generated by jscoverage/JSCover -lib-cov - -# Coverage directory used by tools like istanbul -coverage -*.lcov - -# nyc test coverage -.nyc_output - -# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) -.grunt - -# Bower dependency directory (https://bower.io/) -bower_components - -# node-waf configuration -.lock-wscript - -# Compiled binary addons (https://nodejs.org/api/addons.html) -build/Release - -# Dependency directories -node_modules/ -jspm_packages/ - -# Snowpack dependency directory (https://snowpack.dev/) -web_modules/ - -# TypeScript cache -*.tsbuildinfo - -# Optional npm cache directory -.npm - -# Optional eslint cache -.eslintcache - -# Optional stylelint cache -.stylelintcache - -# Microbundle cache -.rpt2_cache/ -.rts2_cache_cjs/ -.rts2_cache_es/ -.rts2_cache_umd/ - -# Optional REPL history -.node_repl_history - -# Output of 'npm pack' -*.tgz - -# Yarn Integrity file -.yarn-integrity - -# dotenv environment variable files -.env -.env.development.local -.env.test.local -.env.production.local -.env.local - -# parcel-bundler cache (https://parceljs.org/) -.cache -.parcel-cache - -# Next.js build output -.next -out - -# Nuxt.js build / generate output -.nuxt -dist - -# Vite build output -dist-ssr - -# Gatsby files -.cache/ -# Comment in the public line in if your project uses Gatsby and not Next.js -# https://nextjs.org/blog/next-9-1#public-directory-support -# public - -# vuepress build output -.vuepress/dist - -# vuepress v2.x temp and cache directory -.temp -.cache - -# Docusaurus cache and generated files -.docusaurus - -# Serverless directories -.serverless/ - -# FuseBox cache -.fusebox/ - -# DynamoDB Local files -.dynamodb/ - -# TernJS port file -.tern-port - - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - - -# Stores VS Code versions used for testing VS Code extensions -.vscode-test - -# yarn v2 -.yarn/cache -.yarn/unplugged -.yarn/build-state.yml -.yarn/install-state.gz -`; - -export default gitignore; diff --git a/packages/create-toolpad-app/src/templates/indexPage.ts b/packages/create-toolpad-app/src/templates/indexPage.ts deleted file mode 100644 index 5576feda920..00000000000 --- a/packages/create-toolpad-app/src/templates/indexPage.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { Template } from '../types'; - -const indexPage: Template = (options) => { - const authEnabled = options.auth; - const routerType = options.router; - - let imports = `import * as React from 'react'; -import Typography from '@mui/material/Typography';`; - - let sessionHandling = ''; - - let welcomeMessage = `Welcome to Toolpad Core!`; - - if (options.framework === 'nextjs') { - if (authEnabled) { - welcomeMessage = `Welcome to Toolpad, {session?.user?.name || ${options.hasNodemailerProvider || options.hasPasskeyProvider ? `session?.user?.email ||` : ''}'User'}!`; - if (routerType === 'nextjs-app') { - if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - imports += `\nimport { redirect } from 'next/navigation';\nimport { headers } from 'next/headers';`; - } - imports += `\nimport { auth } from '../../auth';`; - sessionHandling = `const session = await auth();`; - if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - sessionHandling += `\nconst currentUrl = (await headers()).get('referer') || process.env.AUTH_URL || 'http://localhost:3000'; - if (!session) { // Get the current URL to redirect to signIn with \`callbackUrl\` - const redirectUrl = new URL('/auth/signin', currentUrl);\nredirectUrl.searchParams.set('callbackUrl', currentUrl);\nredirect(redirectUrl.toString()); - } - `; - } - } else if (routerType === 'nextjs-pages') { - imports += `\nimport { useSession } from 'next-auth/react';`; - sessionHandling = `const { data: session } = useSession();`; - } - } else { - imports += `\nimport Link from 'next/link';`; - welcomeMessage = `Welcome to Toolpad Core!`; - } - } - - const isAsync = authEnabled && routerType === 'nextjs-app' ? 'async ' : ''; - - let requireAuth = ''; - if (authEnabled && routerType === 'nextjs-pages') { - requireAuth = `\n\nHomePage.requireAuth = true;`; - } - - return `${imports} - -export default ${isAsync}function HomePage() { - ${sessionHandling} - - return ( - - ${welcomeMessage} - - ); -}${requireAuth} -`; -}; - -export default indexPage; diff --git a/packages/create-toolpad-app/src/templates/nextConfig.ts b/packages/create-toolpad-app/src/templates/nextConfig.ts deleted file mode 100644 index 1d4f7d7fdb3..00000000000 --- a/packages/create-toolpad-app/src/templates/nextConfig.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Template } from '../types'; - -const nextConfig: Template = (options) => ` - /** @type {import('next').NextConfig} */ - const nextConfig = { - ${options.router === 'nextjs-pages' ? 'transpilePackages: ["next-auth"],' : ''} - }; - export default nextConfig; -`; - -export default nextConfig; diff --git a/packages/create-toolpad-app/src/templates/nextTypes.ts b/packages/create-toolpad-app/src/templates/nextTypes.ts deleted file mode 100644 index 5ec7f842530..00000000000 --- a/packages/create-toolpad-app/src/templates/nextTypes.ts +++ /dev/null @@ -1,8 +0,0 @@ -const nextTypes = `/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. - `; - -export default nextTypes; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts deleted file mode 100644 index 79f71489c87..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/auth.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; -import { kebabToConstant, kebabToPascal } from '@toolpad/utils/strings'; -import { requiresIssuer, requiresTenantId } from './utils'; -import { Template } from '../../../types'; - -const CredentialsProviderTemplate = `Credentials({ - credentials: { - email: { label: 'Email Address', type: 'email' }, - password: { label: 'Password', type: 'password' }, - }, - authorize(c) { - if (c.password !== 'password') { - return null; - } - return { - id: 'test', - name: 'Test User', - email: String(c.email), - }; - }, -}),`; - -const NodemailerTemplate = `Nodemailer({ - server: { - host: process.env.EMAIL_SERVER_HOST, - port: process.env.EMAIL_SERVER_PORT, - auth: { - user: process.env.EMAIL_SERVER_USER, - pass: process.env.EMAIL_SERVER_PASSWORD, - }, - secure: true, - }, - from: process.env.EMAIL_FROM, - }),`; - -const PasskeyTemplate = 'Passkey,'; - -const oAuthProviderTemplate = (provider: SupportedAuthProvider) => ` - ${kebabToPascal(provider)}({ - clientId: process.env.${kebabToConstant(provider)}_CLIENT_ID, - clientSecret: process.env.${kebabToConstant(provider)}_CLIENT_SECRET,${requiresIssuer(provider) ? `\n\t\tissuer: process.env.${kebabToConstant(provider)}_ISSUER,` : ''}${requiresTenantId(provider) ? `\n\t\ttenantId: process.env.${kebabToConstant(provider)}_TENANT_ID,` : ''} - }),`; -const checkEnvironmentVariables = (providers: SupportedAuthProvider[] | undefined) => `${providers - ?.filter((p) => p !== 'credentials') - .map((provider) => { - if (provider === 'nodemailer') { - return `if(!process.env.DATABASE_URL || !process.env.EMAIL_SERVER_HOST) { \nconsole.warn('The Nodemailer provider requires configuring a database and an email server.')\n}`; - } - if (provider === 'passkey') { - return `if(!process.env.DATABASE_URL) { \nconsole.warn('The passkey provider requires configuring a database.')\n}`; - } - return `if(!process.env.${kebabToConstant(provider)}_CLIENT_ID) { - console.warn('Missing environment variable "${kebabToConstant(provider)}_CLIENT_ID"'); -} -if(!process.env.${kebabToConstant(provider)}_CLIENT_SECRET) { - console.warn('Missing environment variable "${kebabToConstant(provider)}_CLIENT_SECRET"'); -}${ - requiresTenantId(provider) - ? ` -if(!process.env.${kebabToConstant(provider)}_TENANT_ID) { - console.warn('Missing environment variable "${kebabToConstant(provider)}_TENANT_ID"'); -}` - : '' - }${ - requiresIssuer(provider) - ? ` -if(!process.env.${kebabToConstant(provider)}_ISSUER) { - console.warn('Missing environment variable "${kebabToConstant(provider)}_ISSUER"'); -}` - : '' - }`; - }) - .join('\n')} -`; - -const auth: Template = (options) => { - const providers = options.authProviders; - - return `import NextAuth from 'next-auth';\n${providers - ?.map( - (provider) => - `import ${kebabToPascal(provider)} from 'next-auth/providers/${provider.toLowerCase()}';`, - ) - .join('\n')} -import type { Provider } from 'next-auth/providers'; -${options.hasNodemailerProvider || options.hasPasskeyProvider ? `\nimport { PrismaAdapter } from '@auth/prisma-adapter';\nimport { prisma } from './prisma';` : ''} - -const providers: Provider[] = [${providers - ?.map((provider) => { - if (provider === 'credentials') { - return CredentialsProviderTemplate; - } - if (provider === 'nodemailer') { - return NodemailerTemplate; - } - if (provider === 'passkey') { - return PasskeyTemplate; - } - return oAuthProviderTemplate(provider); - }) - .join('\n')} -]; - -${checkEnvironmentVariables(providers)} - -export const providerMap = providers.map((provider) => { - if (typeof provider === 'function') { - const providerData = provider(); - return { id: providerData.id, name: providerData.name }; - } - return { id: provider.id, name: provider.name }; -}); - -export const { handlers, auth, signIn, signOut } = NextAuth({ - providers, - ${options.hasNodemailerProvider || options.hasPasskeyProvider ? `\nadapter: PrismaAdapter(prisma),` : ''} - ${options.hasNodemailerProvider || (options.router === 'nextjs-app' && options.hasPasskeyProvider && providers && providers.length > 1) ? `\nsession: { strategy: 'jwt' },` : ''} - ${ - options.hasPasskeyProvider - ? `\nexperimental: { - enableWebAuthn: true, - },` - : '' - } - secret: process.env.AUTH_SECRET, - pages: { - signIn: '/auth/signin', - }, - callbacks: { - authorized({ auth: session, request: { nextUrl } }) { - const isLoggedIn = !!session?.user; - const isPublicPage = nextUrl.pathname.startsWith('/public'); - - if (isPublicPage || isLoggedIn) { - return true; - } - - return false; // Redirect unauthenticated users to login page - }, - }, -}); - `; -}; - -export default auth; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/env.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/env.ts deleted file mode 100644 index 57d5ab57473..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/env.ts +++ /dev/null @@ -1,5 +0,0 @@ -const env = ` -DATABASE_URL = -`; - -export default env; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/envLocal.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/envLocal.ts deleted file mode 100644 index befc0072fb1..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/envLocal.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { kebabToConstant } from '@toolpad/utils/strings'; -import { requiresIssuer, requiresTenantId } from './utils'; -import { Template } from '../../../types'; - -const env: Template = (options) => { - const { authProviders: providers } = options; - const nonCredentialProviders = providers?.filter( - (provider) => provider !== 'credentials' && provider !== 'nodemailer' && provider !== 'passkey', - ); - return ` -# Generate a secret with \`npx auth secret\` -# and replace the value below with it -AUTH_SECRET=secret - -# Add secrets for your auth providers to the .env.local file -${nonCredentialProviders - ?.map( - (provider) => ` -${kebabToConstant(provider)}_CLIENT_ID= -${kebabToConstant(provider)}_CLIENT_SECRET= -${requiresIssuer(provider) ? `${kebabToConstant(provider)}_ISSUER=\n` : ''}${requiresTenantId(provider) ? `${kebabToConstant(provider)}_TENANT_ID=\n` : ''}`, - ) - .join('\n')} - -${ - options.hasNodemailerProvider - ? `EMAIL_SERVER_HOST=\nEMAIL_SERVER_PORT=\nEMAIL_SERVER_USER=\nEMAIL_SERVER_PASSWORD=\nEMAIL_FROM=` - : '' -}`; -}; - -export default env; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/middleware.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/middleware.ts deleted file mode 100644 index 14720ae88ff..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/middleware.ts +++ /dev/null @@ -1,10 +0,0 @@ -const middleware = ` -export { auth as middleware } from './auth'; - -export const config = { - // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher - matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], -}; -`; - -export default middleware; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/actions.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/actions.ts deleted file mode 100644 index 6ed7e9b1e56..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/actions.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Template } from '../../../../types'; - -const actionsTemplate: Template = (options) => { - const { hasCredentialsProvider, hasNodemailerProvider } = options; - - return `'use server'; -import { AuthError } from 'next-auth'; -import type { AuthProvider } from '@toolpad/core'; -import { signIn } from '../../../auth'; - -export default async function serverSignIn(provider: AuthProvider, formData: FormData, callbackUrl?: string) { - try { - return await signIn(provider.id, { - ...(formData && { email: formData.get('email'), password: formData.get('password') }), - redirectTo: callbackUrl ?? '/', - }); - } catch (error) { - if (error instanceof Error && error.message === 'NEXT_REDIRECT') { - ${ - hasNodemailerProvider - ? `if (provider.id === 'nodemailer' && (error as any).digest?.includes('verify-request')) { - return { - success: 'Check your email for a verification link.', - }; - }` - : '' - } - throw error; - } - if (error instanceof AuthError) { - return { - error: ${ - hasCredentialsProvider - ? `error.type === 'CredentialsSignin' ? 'Invalid credentials.' : 'An error with Auth.js occurred.'` - : `'An error with Auth.js occurred.'` - }, - type: error.type, - }; - } - return { - error: 'Something went wrong.', - type: 'UnknownError', - }; - } -}`; -}; - -export default actionsTemplate; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts deleted file mode 100644 index d30264a6fd2..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-app/signInPage.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Template } from '../../../../types'; - -const signInPage: Template = (options) => { - const { hasPasskeyProvider, hasNodemailerProvider } = options; - - return `${hasPasskeyProvider ? "'use client';" : ''} - import * as React from 'react'; -import { SignInPage } from '@toolpad/core/SignInPage'; -${hasPasskeyProvider ? "import { signIn as webauthnSignIn } from 'next-auth/webauthn';" : ''} -${hasPasskeyProvider && hasNodemailerProvider ? `import { getProviders } from "next-auth/react";` : `import { providerMap } from '../../../auth';`} -${hasPasskeyProvider ? `import type { AuthProvider } from '@toolpad/core';` : ''} -${hasPasskeyProvider ? `import serverSignIn from './actions';` : `import signIn from './actions';`} - -${ - hasPasskeyProvider - ? `const signIn = async (provider: AuthProvider, formData: FormData, callbackUrl?: string) => { - if (provider.id === 'passkey') { - try { - return await webauthnSignIn('passkey', { - email: formData.get('email'), - callbackUrl: callbackUrl || '/', - }); - } catch (error) { - console.error(error); - return { - error: (error as Error)?.message || 'Something went wrong', - type: 'WebAuthnError', - }; - } - } - // Use server action for other providers - return serverSignIn(provider, formData, callbackUrl); -};` - : '' -} - -export default function SignIn() { -${ - hasPasskeyProvider && hasNodemailerProvider - ? `const [providerMap, setProviderMap] = React.useState(); - React.useEffect(() => { - const loadProviders = async () => { - const providers = await getProviders(); - const providerList = - Object.entries(providers || {}).map(([id, data]) => ({ - id, - name: data.name, - })) || []; - setProviderMap(providerList); - }; - loadProviders(); - }, []);` - : '' -} - return ( - - ); -}`; -}; - -export default signInPage; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts deleted file mode 100644 index 8fe3c4aefdd..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/nextjs-pages/signIn.ts +++ /dev/null @@ -1,117 +0,0 @@ -import { Template } from '../../../../types'; - -const signIn: Template = (options) => { - const { hasCredentialsProvider, hasNodemailerProvider, hasPasskeyProvider } = options; - - return `import * as React from 'react'; -import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; -import { SignInPage } from '@toolpad/core/SignInPage'; -${hasPasskeyProvider ? "import { signIn as webauthnSignIn } from 'next-auth/webauthn';" : ''} -import { signIn } from 'next-auth/react'; -import { useRouter } from 'next/router'; -import { auth, providerMap } from '../../auth'; - -export default function SignIn({ - providers, -}: InferGetServerSidePropsType) { - const router = useRouter(); - return ( - { - try { - ${ - hasPasskeyProvider - ? `if (provider.id === 'passkey') { - try { - return await webauthnSignIn('passkey', { - email: formData.get('email'), - callbackUrl: callbackUrl || '/', - }); - } catch (error) { - console.error(error); - return { - error: (error as Error)?.message || 'Something went wrong', - type: 'WebAuthnError', - }; - } - }` - : '' - } - const signInResponse = await signIn( - provider.id, { - ...(formData ? { email: formData.get('email'), password: formData.get('password')${hasNodemailerProvider || hasCredentialsProvider ? `, redirect: false` : ''} - } : - { callbackUrl: callbackUrl ?? '/' }) - } - ); - ${ - hasNodemailerProvider - ? `\n// For the nodemailer provider, we want to return a success message - // instead of redirecting to a \`verify-request\` page - if ( - provider.id === "nodemailer" - ) { - return { - success: "Check your email for a verification link.", - }; - } - ` - : '' - } - if (signInResponse && signInResponse.error) { - // Handle Auth.js errors - return { - error: ${ - hasCredentialsProvider - ? `signInResponse.error === 'CredentialsSignin' ? 'Invalid credentials.' - : 'An error with Auth.js occurred.'` - : `'An error with Auth.js occurred'` - }, - type: signInResponse.error, - }; - } - ${ - hasCredentialsProvider || hasNodemailerProvider - ? ` - if(provider.id === "credentials" || provider.id === "nodemailer") { - router.push(callbackUrl ?? '/'); - }` - : '' - } - return {}; - } catch (error) { - // An error boundary must exist to handle unknown errors - return { - error: 'Something went wrong.', - type: 'UnknownError', - }; - } - }} - /> - ); -} - -SignIn.getLayout = (page: React.ReactNode) => page; - -SignIn.requireAuth = false; - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const session = await auth(context); - - // If the user is already logged in, redirect. - // Note: Make sure not to redirect to the same page - // To avoid an infinite loop! - if (session) { - return { redirect: { destination: '/' } }; - } - - return { - props: { - providers: providerMap, - }, - }; -}`; -}; - -export default signIn; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/prisma.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/prisma.ts deleted file mode 100644 index 34d03e0ef8c..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/prisma.ts +++ /dev/null @@ -1,11 +0,0 @@ -const prisma = `import { PrismaClient } from '@prisma/client'; - -const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }; - -export const prisma = globalForPrisma.prisma || new PrismaClient(); - -if (process.env.NODE_ENV !== 'production') { - globalForPrisma.prisma = prisma; -}`; - -export default prisma; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/route.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/route.ts deleted file mode 100644 index 50e6ce8bdb5..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/route.ts +++ /dev/null @@ -1,6 +0,0 @@ -const route = `import { handlers } from '../../../../auth'; - -export const { GET, POST } = handlers; -`; - -export default route; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/schemaPrisma.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/schemaPrisma.ts deleted file mode 100644 index 4fb2b066f13..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/schemaPrisma.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { Template } from '../../../types'; - -const schemaPrisma: Template = (options) => ` - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -generator client { - provider = "prisma-client-js" -} - -model User { - id String @id @default(cuid()) - name String? - email String @unique - emailVerified DateTime? - image String? - accounts Account[] - sessions Session[] - ${options.hasPasskeyProvider ? 'Authenticator Authenticator[]' : ''} - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model Account { - userId String - type String - provider String - providerAccountId String - refresh_token String? - access_token String? - expires_at Int? - token_type String? - scope String? - id_token String? - session_state String? - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@id([provider, providerAccountId]) -} - -model Session { - sessionToken String @unique - userId String - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - createdAt DateTime @default(now()) - updatedAt DateTime @updatedAt -} - -model VerificationToken { - identifier String - token String - expires DateTime - - @@id([identifier, token]) -} - -${ - options.hasPasskeyProvider - ? ` -model Authenticator { - credentialID String @unique - userId String - providerAccountId String - credentialPublicKey String - counter Int - credentialDeviceType String - credentialBackedUp Boolean - transports String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@id([userId, credentialID]) -}` - : '' -} -`; - -export default schemaPrisma; diff --git a/packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts b/packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts deleted file mode 100644 index 719277b1d92..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/auth/utils.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; - -export function requiresIssuer(provider: SupportedAuthProvider) { - return ( - provider === 'cognito' || - provider === 'fusionauth' || - provider === 'keycloak' || - provider === 'okta' - ); -} - -export function requiresTenantId(provider: SupportedAuthProvider) { - return provider === 'microsoft-entra-id' || provider === 'fusionauth'; -} diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts deleted file mode 100644 index edc93c86b54..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/dashboardLayout.ts +++ /dev/null @@ -1,14 +0,0 @@ -const dashboardLayout = `import * as React from 'react'; -import { DashboardLayout } from '@toolpad/core/DashboardLayout'; -import { PageContainer } from '@toolpad/core/PageContainer'; - -export default function Layout(props: { children: React.ReactNode }) { - return ( - - {props.children} - - ); -} -`; - -export default dashboardLayout; diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts deleted file mode 100644 index 9ab535e257a..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-app/rootLayout.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { Template } from '../../../types'; - -const rootLayout: Template = (options) => { - const authEnabled = options.auth; - - return `import * as React from 'react'; -import { NextAppProvider } from '@toolpad/core/nextjs'; -import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; -${authEnabled ? '' : `import LinearProgress from '@mui/material/LinearProgress'`} -import type { Navigation } from '@toolpad/core/AppProvider'; -${ - authEnabled - ? `import { SessionProvider, signIn, signOut } from 'next-auth/react'; -import { auth } from '../auth';` - : '' -} -import theme from '../theme'; - -const NAVIGATION: Navigation = [ - { - kind: 'header', - title: 'Main items', - }, - { - segment: '', - title: 'Dashboard', - icon: , - }, - { - segment: 'orders', - title: 'Orders', - icon: , - }, -]; - -const BRANDING = { - title: 'My Toolpad Core Next.js App', -}; - -${ - authEnabled - ? ` -const AUTHENTICATION = { - signIn, - signOut, -}; -` - : '' -} - -export default ${authEnabled ? 'async ' : ''}function RootLayout(props: { children: React.ReactNode }) { - ${authEnabled ? `const session = await auth();` : ''} - - return ( - - - ${authEnabled ? '' : ''} - - ${authEnabled ? '' : '}>'} - - {props.children} - - ${authEnabled ? '' : ''} - - ${authEnabled ? '' : ''} - - - ); -} -`; -}; - -export default rootLayout; diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts deleted file mode 100644 index f408aabe703..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/app.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { Template } from '../../../types'; - -const app: Template = (options) => { - const authEnabled = options.auth; - - return `import * as React from 'react'; -import { NextAppProvider } from '@toolpad/core/nextjs'; -import { DashboardLayout } from '@toolpad/core/DashboardLayout'; -import { PageContainer } from '@toolpad/core/PageContainer'; -import Head from 'next/head'; -import { AppCacheProvider } from '@mui/material-nextjs/v14-pagesRouter'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; -import type { NextPage } from 'next'; -import type { AppProps } from 'next/app'; -import type { Navigation } from '@toolpad/core/AppProvider'; -${ - authEnabled - ? `import { SessionProvider, signIn, signOut, useSession } from 'next-auth/react'; -import LinearProgress from '@mui/material/LinearProgress';` - : '' -} -${ - options.hasNodemailerProvider || options.hasPasskeyProvider - ? `import { useRouter } from 'next/router';` - : '' -} -import theme from '../theme'; - -export type NextPageWithLayout

= NextPage & { - getLayout?: (page: React.ReactElement) => React.ReactNode; - ${authEnabled ? 'requireAuth?: boolean;' : ''} -}; - -type AppPropsWithLayout = AppProps & { - Component: NextPageWithLayout; -}; - -const NAVIGATION: Navigation = [ - { - kind: 'header', - title: 'Main items', - }, - { - segment: '', - title: 'Dashboard', - icon: , - }, - { - segment: 'orders', - title: 'Orders', - icon: , - }, -]; - -const BRANDING = { - title: 'My Toolpad Core Next.js Pages App', -}; - -${ - authEnabled - ? ` -const AUTHENTICATION = { - signIn, - signOut, -}; - -function RequireAuth({ children }: { children: React.ReactNode }) { - const { status${options.hasNodemailerProvider || options.hasPasskeyProvider ? ',data' : ''} } = useSession(); - ${options.hasNodemailerProvider || options.hasPasskeyProvider ? `const router = useRouter()` : ''} - if (status === 'loading') { - return ; - } - - ${ - options.hasNodemailerProvider || options.hasPasskeyProvider - ? `if (!data) { - // Redirect to sign-in page - router.push("/api/auth/signin"); - return ; - }` - : '' - } - - - return children; -} -` - : '' -} - -function getDefaultLayout(page: React.ReactElement) { - return ( - - {page} - - ); -} - -function AppLayout({ children }: { children: React.ReactNode }) { - ${authEnabled ? `const { data: session } = useSession();` : ''} - return ( - - - - - - {children} - - - ); -} - -export default function App(props: AppPropsWithLayout) { - const { - Component, - pageProps${authEnabled ? `: { session, ...pageProps }` : ''}, - } = props; - - const getLayout = Component.getLayout ?? getDefaultLayout; - ${ - authEnabled - ? `const requireAuth = Component.requireAuth ?? true; - - let pageContent = getLayout(); - if (requireAuth) { - pageContent = {pageContent}; - }` - : `let pageContent = getLayout();` - } - pageContent = {pageContent}; - - return ( - - ${authEnabled ? `` : ''} - {pageContent} - ${authEnabled ? `` : ''} - - ); -} -`; -}; - -export default app; diff --git a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/document.ts b/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/document.ts deleted file mode 100644 index 7512201a61b..00000000000 --- a/packages/create-toolpad-app/src/templates/nextjs/nextjs-pages/document.ts +++ /dev/null @@ -1,30 +0,0 @@ -const document = `import * as React from 'react'; -import { Html, Head, Main, NextScript, DocumentProps, DocumentContext } from 'next/document'; -import { - DocumentHeadTags, - DocumentHeadTagsProps, - documentGetInitialProps, -} from '@mui/material-nextjs/v14-pagesRouter'; - -export default function Document(props: DocumentProps & DocumentHeadTagsProps) { - return ( - - - - - - -

- - - - ); -} - -Document.getInitialProps = async (ctx: DocumentContext) => { - const finalProps = await documentGetInitialProps(ctx); - return finalProps; -}; -`; - -export default document; diff --git a/packages/create-toolpad-app/src/templates/ordersPage.ts b/packages/create-toolpad-app/src/templates/ordersPage.ts deleted file mode 100644 index 975840fd56e..00000000000 --- a/packages/create-toolpad-app/src/templates/ordersPage.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Template } from '../types'; - -const ordersPage: Template = (options) => { - const authEnabled = options.auth; - const routerType = options.router; - - let imports = `import * as React from 'react'; -import Typography from '@mui/material/Typography';`; - - let sessionHandling = ''; - - if (authEnabled) { - if (routerType === 'nextjs-app') { - if (options.hasNodemailerProvider || options.hasPasskeyProvider) { - imports += `\nimport { redirect } from 'next/navigation';\nimport { headers } from 'next/headers';\nimport { auth } from '../../../auth';`; - sessionHandling = `const session = await auth(); - const currentUrl = (await headers()).get('referer') || process.env.AUTH_URL || 'http://localhost:3000'; - - if (!session) { - const redirectUrl = new URL('/auth/signin', currentUrl); - redirectUrl.searchParams.set('callbackUrl', currentUrl); - - redirect(redirectUrl.toString()); - } - `; - } - } - } - - const isAsync = authEnabled && routerType === 'nextjs-app' ? 'async ' : ''; - - let requireAuth = ''; - if (authEnabled && routerType === 'nextjs-pages') { - requireAuth = `\n\nOrdersPage.requireAuth = true;`; - } - - return `${imports} - - -export default ${isAsync}function OrdersPage() { - ${sessionHandling} - - return ( - - Welcome to the Toolpad orders! - - ); -}${requireAuth} -`; -}; - -export default ordersPage; diff --git a/packages/create-toolpad-app/src/templates/packageJson.ts b/packages/create-toolpad-app/src/templates/packageJson.ts deleted file mode 100644 index 62564e7b17b..00000000000 --- a/packages/create-toolpad-app/src/templates/packageJson.ts +++ /dev/null @@ -1,111 +0,0 @@ -import { PackageJsonTemplate } from '../types'; - -const packageJson: PackageJsonTemplate = (options) => { - const { - name: appName, - projectType, - router: routerType, - framework, - auth: authOption, - coreVersion, - hasNodemailerProvider, - hasPasskeyProvider, - } = options; - - if (projectType === 'studio') { - return { - name: appName, - version: '0.1.0', - scripts: { - dev: 'toolpad-studio dev', - build: 'toolpad-studio build', - start: 'toolpad-studio start', - }, - dependencies: { - '@toolpad/studio': 'latest', - }, - }; - } - - const dependencies: Record = { - react: '^19', - 'react-dom': '^19', - '@toolpad/core': coreVersion ?? 'latest', - '@mui/material': '^6', - '@mui/material-nextjs': '^6', - '@mui/icons-material': '^6', - '@emotion/react': '^11', - '@emotion/styled': '^11', - }; - - const devDependencies: Record = { - typescript: '^5', - '@types/react': '^18', - '@types/react-dom': '^18', - eslint: '^8', - }; - - if (framework === 'nextjs') { - dependencies.next = '^15'; - devDependencies['eslint-config-next'] = '^15'; - devDependencies['@types/node'] = '^20'; - } else if (framework === 'vite') { - dependencies['react-router'] = '^7'; - devDependencies['@vitejs/plugin-react'] = '^4.3.2'; - devDependencies.vite = '^5.4.8'; - } - - if (routerType === 'nextjs-pages') { - dependencies['@emotion/cache'] = '^11'; - dependencies['@emotion/server'] = '^11'; - } - - if (authOption) { - if (framework === 'nextjs') { - dependencies['next-auth'] = '5.0.0-beta.25'; - } else if (framework === 'vite') { - dependencies.firebase = '^11'; - } - } - - if (hasNodemailerProvider || hasPasskeyProvider) { - dependencies['@prisma/client'] = '^5'; - dependencies['@auth/prisma-adapter'] = '^2'; - } - - if (hasNodemailerProvider) { - dependencies.nodemailer = '^6'; - } - - if (hasPasskeyProvider) { - dependencies['@simplewebauthn/browser'] = '^9'; - dependencies['@simplewebauthn/server'] = '^9'; - } - - if (hasNodemailerProvider || hasPasskeyProvider) { - devDependencies.prisma = '^5'; - } - - const scripts = - framework === 'nextjs' - ? { - dev: 'next dev', - build: 'next build', - start: 'next start', - lint: 'next lint', - } - : { - dev: 'vite', - preview: 'vite preview', - }; - - return { - name: appName, - version: '0.1.0', - scripts, - dependencies, - devDependencies, - }; -}; - -export default packageJson; diff --git a/packages/create-toolpad-app/src/templates/readme.ts b/packages/create-toolpad-app/src/templates/readme.ts deleted file mode 100644 index 2104e11b593..00000000000 --- a/packages/create-toolpad-app/src/templates/readme.ts +++ /dev/null @@ -1,26 +0,0 @@ -const readme = ` -# Create Toolpad App - -This is a [Next.js](https://nextjs.org/) project bootstrapped with [\`create-toolpad-app\`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Setup - -Run \`npx auth secret\` to generate a secret and replace the value in the .env.local file with it. - -Add the CLIENT_ID and CLIENT_SECRET from your OAuth provider to the .env.local file. - -## Getting Started - -First, run the development server: \`npm run dev\` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. -`; - -export default readme; diff --git a/packages/create-toolpad-app/src/templates/studio/packageJson.ts b/packages/create-toolpad-app/src/templates/studio/packageJson.ts deleted file mode 100644 index bf3b9a78590..00000000000 --- a/packages/create-toolpad-app/src/templates/studio/packageJson.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { PackageJsonTemplate } from '../../types'; - -const packageJson: PackageJsonTemplate = (options) => ({ - name: options.name, - version: '0.1.0', - scripts: { - dev: 'toolpad-studio dev', - build: 'toolpad-studio build', - start: 'toolpad-studio start', - }, - dependencies: { - '@toolpad/studio': 'latest', - }, -}); - -export default packageJson; diff --git a/packages/create-toolpad-app/src/templates/theme.ts b/packages/create-toolpad-app/src/templates/theme.ts deleted file mode 100644 index 8bc9e494eda..00000000000 --- a/packages/create-toolpad-app/src/templates/theme.ts +++ /dev/null @@ -1,15 +0,0 @@ -const theme = ` - "use client"; - import { createTheme } from '@mui/material/styles'; - - const theme = createTheme({ - cssVariables: { - colorSchemeSelector: 'data-toolpad-color-scheme', - }, - colorSchemes: { light: true, dark: true }, - }); - - export default theme; - `; - -export default theme; diff --git a/packages/create-toolpad-app/src/templates/tsConfig.ts b/packages/create-toolpad-app/src/templates/tsConfig.ts deleted file mode 100644 index 44d7994f986..00000000000 --- a/packages/create-toolpad-app/src/templates/tsConfig.ts +++ /dev/null @@ -1,29 +0,0 @@ -export const tsconfig = `{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] - } - `; - -export default tsconfig; diff --git a/packages/create-toolpad-app/src/templates/vite/App.ts b/packages/create-toolpad-app/src/templates/vite/App.ts deleted file mode 100644 index 7afe06e45d1..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/App.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { Template } from '../../types'; - -const appTemplate: Template = (options) => { - return `import * as React from 'react'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; -import { Outlet } from 'react-router'; -${ - options.auth - ? `import { ReactRouterAppProvider } from '@toolpad/core/react-router'; -import type { Navigation, Authentication } from '@toolpad/core/AppProvider'; -import { firebaseSignOut, onAuthStateChanged } from './firebase/auth'; -import SessionContext, { type Session } from './SessionContext';` - : `import { ReactRouterAppProvider } from '@toolpad/core/react-router';\nimport type { Navigation } from '@toolpad/core/AppProvider';` -} - -const NAVIGATION: Navigation = [ - { - kind: 'header', - title: 'Main items', - }, - { - title: 'Dashboard', - icon: , - }, - { - segment: 'orders', - title: 'Orders', - icon: , - }, -]; - -const BRANDING = { - title: ${JSON.stringify(options.name || 'My Toolpad Core App')}, -}; -${ - options.auth - ? ` -const AUTHENTICATION: Authentication = { - signIn: () => {}, - signOut: firebaseSignOut, -};` - : '' -} - -export default function App() { - ${ - options.auth - ? `const [session, setSession] = React.useState(null); - const [loading, setLoading] = React.useState(true); - - const sessionContextValue = React.useMemo( - () => ({ - session, - setSession, - loading, - }), - [session, loading], - ); - - React.useEffect(() => { - const unsubscribe = onAuthStateChanged((user) => { - if (user) { - setSession({ - user: { - name: user.name || '', - email: user.email || '', - image: user.image || '', - }, - }); - } else { - setSession(null); - } - setLoading(false); - }); - - return () => unsubscribe(); - }, []); - - return ( - - - - - - );` - : ` - return ( - - - - );` - } -}`; -}; - -export default appTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/SessionContext.ts b/packages/create-toolpad-app/src/templates/vite/SessionContext.ts deleted file mode 100644 index d60bc9e8464..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/SessionContext.ts +++ /dev/null @@ -1,25 +0,0 @@ -export default `import * as React from 'react'; - -export interface Session { - user: { - name?: string; - email?: string; - image?: string; - }; -} - -interface SessionContextType { - session: Session | null; - setSession: (session: Session | null) => void; - loading: boolean; -} - -const SessionContext = React.createContext({ - session: null, - setSession: () => {}, - loading: true, -}); - -export default SessionContext; - -export const useSession = () => React.useContext(SessionContext);`; diff --git a/packages/create-toolpad-app/src/templates/vite/auth/env.ts b/packages/create-toolpad-app/src/templates/vite/auth/env.ts deleted file mode 100644 index 2f72af4cc1a..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/auth/env.ts +++ /dev/null @@ -1,8 +0,0 @@ -const envTemplate = `VITE_FIREBASE_API_KEY="" -VITE_FIREBASE_AUTH_DOMAIN="" -VITE_FIREBASE_PROJECT_ID="" -VITE_FIREBASE_STORAGE_BUCKET="" -VITE_FIREBASE_MESSAGE_SENDER_ID="" -VITE_FIREBASE_APP_ID=""`; - -export default envTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/auth/firebase.ts b/packages/create-toolpad-app/src/templates/vite/auth/firebase.ts deleted file mode 100644 index 0d7a556a625..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/auth/firebase.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Template } from '../../../types'; - -const firebaseAuthTemplate: Template = (options) => { - const { hasCredentialsProvider = true } = options; - - const hasGoogleProvider = true; - const hasGithubProvider = true; - - return `import { - ${[ - ...(hasGoogleProvider ? ['GoogleAuthProvider,'] : []), - ...(hasGithubProvider ? ['GithubAuthProvider,'] : []), - 'setPersistence,', - 'browserSessionPersistence,', - ...(hasCredentialsProvider ? ['signInWithEmailAndPassword,'] : []), - 'signInWithPopup,', - 'signOut,', - ].join('\n ')} -} from 'firebase/auth'; -import { firebaseAuth } from './firebaseConfig'; - -${hasGoogleProvider ? 'const googleProvider = new GoogleAuthProvider();' : ''} -${hasGithubProvider ? 'const githubProvider = new GithubAuthProvider();' : ''} - -${ - hasGoogleProvider - ? `// Sign in with Google functionality -export const signInWithGoogle = async () => { - try { - return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { - const result = await signInWithPopup(firebaseAuth, googleProvider); - return { - success: true, - user: result.user, - error: null, - }; - }); - } catch (error: any) { - return { - success: false, - user: null, - error: error.message, - }; - } -};` - : '' -} - -${ - hasGithubProvider - ? `// Sign in with GitHub functionality -export const signInWithGithub = async () => { - try { - return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { - const result = await signInWithPopup(firebaseAuth, githubProvider); - return { - success: true, - user: result.user, - error: null, - }; - }); - } catch (error: any) { - return { - success: false, - user: null, - error: error.message, - }; - } -};` - : '' -} - -${ - hasCredentialsProvider - ? `// Sign in with email and password -export async function signInWithCredentials(email: string, password: string) { - try { - return setPersistence(firebaseAuth, browserSessionPersistence).then(async () => { - const userCredential = await signInWithEmailAndPassword(firebaseAuth, email, password); - return { - success: true, - user: userCredential.user, - error: null, - }; - }); - } catch (error: any) { - return { - success: false, - user: null, - error: error.message || 'Failed to sign in with email/password', - }; - } -}` - : '' -} - -// Sign out functionality -export const firebaseSignOut = async () => { - try { - await signOut(firebaseAuth); - return { success: true }; - } catch (error: any) { - return { - success: false, - error: error.message, - }; - } -}; - -// Auth state observer -export const onAuthStateChanged = (callback: (user: any) => void) => { - return firebaseAuth.onAuthStateChanged(callback); -};`; -}; - -export default firebaseAuthTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/auth/firebaseConfig.ts b/packages/create-toolpad-app/src/templates/vite/auth/firebaseConfig.ts deleted file mode 100644 index 905cea709dd..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/auth/firebaseConfig.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { Template } from '../../../types'; - -const firebaseConfigTemplate: Template = () => `import { initializeApp } from 'firebase/app'; -import { getAuth } from 'firebase/auth'; - -const requiredEnvVars = { - VITE_FIREBASE_API_KEY: import.meta.env.VITE_FIREBASE_API_KEY, - VITE_FIREBASE_AUTH_DOMAIN: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN, - VITE_FIREBASE_PROJECT_ID: import.meta.env.VITE_FIREBASE_PROJECT_ID, - VITE_FIREBASE_STORAGE_BUCKET: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET, - VITE_FIREBASE_MESSAGE_SENDER_ID: import.meta.env.VITE_FIREBASE_MESSAGE_SENDER_ID, - VITE_FIREBASE_APP_ID: import.meta.env.VITE_FIREBASE_APP_ID, -}; - -// Check for missing environment variables -Object.entries(requiredEnvVars).forEach(([key, value]) => { - if (!value) { - console.warn(\`Missing required environment variable: \${key}\`); - } -}); - -const app = initializeApp({ - apiKey: requiredEnvVars.VITE_FIREBASE_API_KEY, - authDomain: requiredEnvVars.VITE_FIREBASE_AUTH_DOMAIN, - projectId: requiredEnvVars.VITE_FIREBASE_PROJECT_ID, - storageBucket: requiredEnvVars.VITE_FIREBASE_STORAGE_BUCKET, - messagingSenderId: requiredEnvVars.VITE_FIREBASE_MESSAGE_SENDER_ID, - appId: requiredEnvVars.VITE_FIREBASE_APP_ID, -}); - -export const firebaseAuth = getAuth(app); -export default app;`; - -export default firebaseConfigTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/auth/signin.ts b/packages/create-toolpad-app/src/templates/vite/auth/signin.ts deleted file mode 100644 index bbb0d580cf3..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/auth/signin.ts +++ /dev/null @@ -1,101 +0,0 @@ -import { Template } from '../../../types'; - -const signinTemplate: Template = (options) => { - const { hasCredentialsProvider = true } = options; - - const hasGoogleProvider = options.authProviders?.includes('google'); - const hasGithubProvider = options.authProviders?.includes('github'); - - const providers = [ - ...(hasGoogleProvider ? [`{ id: 'google', name: 'Google' }`] : []), - ...(hasGithubProvider ? [`{ id: 'github', name: 'GitHub' }`] : []), - ...(hasCredentialsProvider ? [`{ id: 'credentials', name: 'Credentials' }`] : []), - ]; - - return `'use client'; -import * as React from 'react'; -import Alert from '@mui/material/Alert'; -import LinearProgress from '@mui/material/LinearProgress'; -import { SignInPage } from '@toolpad/core/SignInPage'; -import { Navigate, useNavigate } from 'react-router'; -import { useSession, type Session } from '../SessionContext'; -import { ${[ - hasGoogleProvider && 'signInWithGoogle', - hasGithubProvider && 'signInWithGithub', - hasCredentialsProvider && 'signInWithCredentials', - ] - .filter(Boolean) - .join(', ')} } from '../firebase/auth'; - - -export default function SignIn() { - const { session, setSession, loading } = useSession(); - const navigate = useNavigate(); - - if (loading) { - return ; - } - - if (session) { - return ; - } - - return ( - { - let result; - try { - ${ - hasGoogleProvider - ? `if (provider.id === 'google') { - result = await signInWithGoogle(); - }` - : '' - } - ${ - hasGithubProvider - ? `if (provider.id === 'github') { - result = await signInWithGithub(); - }` - : '' - } - ${ - hasCredentialsProvider - ? `if (provider.id === 'credentials') { - const email = formData?.get('email') as string; - const password = formData?.get('password') as string; - - if (!email || !password) { - return { error: 'Email and password are required' }; - } - - result = await signInWithCredentials(email, password); - }` - : '' - } - - if (result?.success && result?.user) { - const userSession: Session = { - user: { - name: result.user.displayName || '', - email: result.user.email || '', - image: result.user.photoURL || '', - }, - }; - setSession(userSession); - navigate(callbackUrl || '/', { replace: true }); - return {}; - } - return { error: result?.error || 'Failed to sign in' }; - } catch (error) { - return { error: error instanceof Error ? error.message : 'An error occurred' }; - } - }} - - /> - ); -}`; -}; - -export default signinTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts b/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts deleted file mode 100644 index d78e250a7c3..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/dashboardLayout.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Template } from '../../types'; - -const dashboardTemplate: Template = (options) => { - const { auth } = options; - - return `import * as React from 'react'; -${ - auth - ? `import LinearProgress from '@mui/material/LinearProgress'; -import { Outlet, Navigate, useLocation } from 'react-router';` - : `import { Outlet } from 'react-router';` -} -import { DashboardLayout } from '@toolpad/core/DashboardLayout'; -import { PageContainer } from '@toolpad/core/PageContainer'; -${ - auth - ? `import { Account } from '@toolpad/core/Account'; - -import { useSession } from '../SessionContext'; - -function CustomAccount() { - return ( - - ); -}` - : '' -} - -export default function Layout() { - ${ - auth - ? `const { session, loading } = useSession(); - const location = useLocation(); - - if (loading) { - return ( -
- -
- ); - } - - if (!session) { - const redirectTo = \`/sign-in?callbackUrl=\${encodeURIComponent(location.pathname)}\`; - return ; - }` - : '' - } - - return ( - - - - - - ); -}`; -}; - -export default dashboardTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/html.ts b/packages/create-toolpad-app/src/templates/vite/html.ts deleted file mode 100644 index 5ba589de985..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/html.ts +++ /dev/null @@ -1,12 +0,0 @@ -export default ` - - - - - Toolpad Core App - - -
- - -`; diff --git a/packages/create-toolpad-app/src/templates/vite/main.ts b/packages/create-toolpad-app/src/templates/vite/main.ts deleted file mode 100644 index a7e9ce9728b..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/main.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { Template } from '../../types'; - -const mainTemplate: Template = (options) => { - const { auth } = options; - - return `import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import { createBrowserRouter, RouterProvider } from 'react-router'; -import App from './App'; -import Layout from './layouts/dashboard'; -import DashboardPage from './pages'; -import OrdersPage from './pages/orders'; -${auth ? `import SignInPage from './pages/signin';` : ''} - -const router = createBrowserRouter([ - { - Component: App, - children: [ - { - path: '/', - Component: Layout, - children: [ - { - path: '', - Component: DashboardPage, - }, - { - path: 'orders', - Component: OrdersPage, - }, - ], - },${ - auth - ? ` - { - path: '/sign-in', - Component: SignInPage, - },` - : '' - } - ], - }, -]); - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -);`; -}; - -export default mainTemplate; diff --git a/packages/create-toolpad-app/src/templates/vite/viteConfig.ts b/packages/create-toolpad-app/src/templates/vite/viteConfig.ts deleted file mode 100644 index 83bdbb57893..00000000000 --- a/packages/create-toolpad-app/src/templates/vite/viteConfig.ts +++ /dev/null @@ -1,7 +0,0 @@ -export default `import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], -});`; diff --git a/packages/create-toolpad-app/src/types.ts b/packages/create-toolpad-app/src/types.ts deleted file mode 100644 index e4ed06d01af..00000000000 --- a/packages/create-toolpad-app/src/types.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { SupportedAuthProvider } from '@toolpad/core/SignInPage'; -import { PackageJson } from './packageType'; - -export type SupportedRouter = 'nextjs-app' | 'nextjs-pages'; -export type PackageManager = 'npm' | 'pnpm' | 'yarn'; -export type SupportedFramework = 'nextjs' | 'vite'; - -type ProjectType = 'core' | 'studio'; - -export interface GenerateProjectOptions { - name: string; - absolutePath: string; - auth?: boolean; - authProviders?: SupportedAuthProvider[]; - hasNodemailerProvider?: boolean; - hasCredentialsProvider?: boolean; - hasPasskeyProvider?: boolean; - install?: boolean; - router?: SupportedRouter; - framework?: SupportedFramework; - coreVersion?: string; - projectType?: ProjectType; - packageManager: PackageManager; -} - -export type Template = (options: GenerateProjectOptions) => string; - -export type PackageJsonTemplate = (options: GenerateProjectOptions) => PackageJson; diff --git a/packages/create-toolpad-app/src/validation.ts b/packages/create-toolpad-app/src/validation.ts deleted file mode 100644 index 3d6e676404d..00000000000 --- a/packages/create-toolpad-app/src/validation.ts +++ /dev/null @@ -1,65 +0,0 @@ -import * as fs from 'fs/promises'; -import { constants as fsConstants } from 'fs'; -import chalk from 'chalk'; -import { errorFrom } from '@toolpad/utils/errors'; -import { bashResolvePath } from '@toolpad/utils/cli'; - -const VALID_FILES = [ - '.DS_Store', - '.git', - '.gitattributes', - '.gitignore', - '.gitlab-ci.yml', - '.hg', - '.hgcheck', - '.hgignore', - '.idea', - '.npmignore', - '.travis.yml', - 'LICENSE', - 'Thumbs.db', - 'docs', - 'mkdocs.yml', - 'npm-debug.log', - 'yarn-debug.log', - 'yarn-error.log', - 'yarnrc.yml', - '.yarn', -]; - -export async function isFolderEmpty(pathDir: string): Promise { - const conflicts = await fs.readdir(pathDir); - - conflicts.filter((file) => !VALID_FILES.includes(file)).filter((file) => !/\.iml$/.test(file)); - - return conflicts.length === 0; -} - -export async function validatePath( - relativePath: string, - emptyCheck?: boolean, -): Promise { - const absolutePath = bashResolvePath(relativePath); - - try { - await fs.access(absolutePath, fsConstants.F_OK); - - if (emptyCheck) { - if (await isFolderEmpty(absolutePath)) { - return true; - } - - return `${chalk.red('error')} - The directory at ${chalk.cyan( - absolutePath, - )} contains files that could conflict. Either use a new directory, or remove conflicting files.`; - } - return true; - } catch (rawError: unknown) { - const error = errorFrom(rawError); - if (error.code === 'ENOENT') { - await fs.mkdir(absolutePath, { recursive: true }); - return true; - } - throw error; - } -} diff --git a/packages/create-toolpad-app/src/writeFiles.ts b/packages/create-toolpad-app/src/writeFiles.ts deleted file mode 100644 index 5d35d4dcba5..00000000000 --- a/packages/create-toolpad-app/src/writeFiles.ts +++ /dev/null @@ -1,22 +0,0 @@ -import path from 'path'; -import fs from 'fs/promises'; - -export default async function writeFiles( - absolutePath: string, - files: Map, -): Promise { - // Get all directories and deduplicate - const dirs = new Set(Array.from(files.keys(), (filePath) => path.dirname(filePath))); - - // Create directories, use recursive option to create parent directories - await Promise.all( - Array.from(dirs, (dirPath) => fs.mkdir(path.join(absolutePath, dirPath), { recursive: true })), - ); - - // Write all the files - await Promise.all( - Array.from(files.entries(), ([filePath, { content }]) => - fs.writeFile(path.join(absolutePath, filePath), content), - ), - ); -} diff --git a/packages/create-toolpad-app/tests/index.spec.ts b/packages/create-toolpad-app/tests/index.spec.ts deleted file mode 100644 index 8fb12c5e802..00000000000 --- a/packages/create-toolpad-app/tests/index.spec.ts +++ /dev/null @@ -1,396 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import * as url from 'url'; -import readline from 'readline'; -import { Readable } from 'stream'; -import { execa } from 'execa'; -import { test, expect, afterEach } from 'vitest'; -import * as os from 'os'; -import terminate from 'terminate'; - -type ExecaChildProcess = ReturnType; - -const TEST_TIMEOUT = process.env.CI ? 60000 : 600000; - -const currentDirectory = url.fileURLToPath(new URL('.', import.meta.url)); - -const cliPath = path.resolve(currentDirectory, '../dist/index.js'); - -let testDir: string | undefined; -let cp: ExecaChildProcess | undefined; -let toolpadProcess: ExecaChildProcess | undefined; - -async function waitForMatch(input: Readable, regex: RegExp): Promise { - return new Promise((resolve, reject) => { - const rl = readline.createInterface({ input }); - - rl.on('line', (line) => { - const match = regex.exec(line); - if (match) { - rl.close(); - input.resume(); - resolve(match); - } - }); - rl.on('error', (err) => reject(err)); - rl.on('end', () => resolve(null)); - }); -} - -async function waitForPromptAndRespond( - cpr: ExecaChildProcess, - regex: RegExp, - response: string, -): Promise { - return new Promise((resolve) => { - const onData = (data: Buffer) => { - const output = data.toString(); - // Check if the output matches the regex - if (regex.test(output)) { - // Remove the listener to prevent it from being called again - cpr.stdout?.removeListener('data', onData); - // Write the response to the stdin - cpr.stdin?.write(response); - cpr.stdin?.write('\n'); - resolve(); - } - }; - cpr.stdout?.on('data', onData); - }); -} - -test( - 'create-toolpad-app can bootstrap a Toolpad Studio app', - async () => { - testDir = await fs.mkdtemp(path.resolve(os.tmpdir(), './test-app-')); - cp = execa(cliPath, [testDir, '--studio'], { - cwd: currentDirectory, - }); - cp.stdout?.pipe(process.stdout); - cp.stderr?.pipe(process.stderr); - const result = await cp; - expect(result.stdout).toMatch('Run the following to get started'); - const packageJsonContent = await fs.readFile(path.resolve(testDir, './package.json'), { - encoding: 'utf-8', - }); - const packageJson = JSON.parse(packageJsonContent); - expect(packageJson).toEqual( - expect.objectContaining({ - dependencies: expect.objectContaining({ - '@toolpad/studio': expect.any(String), - }), - scripts: expect.objectContaining({ - build: 'toolpad-studio build', - dev: 'toolpad-studio dev', - start: 'toolpad-studio start', - }), - }), - ); - - // check that file exists or not in the directory - const gitignore = await fs.readFile(path.resolve(testDir, './.gitignore'), { - encoding: 'utf-8', - }); - - expect(gitignore.length).toBeGreaterThan(0); - - toolpadProcess = execa('pnpm', ['dev', '--create'], { - cwd: testDir, - env: { - FORCE_COLOR: '0', - BROWSER: 'none', - }, - }); - toolpadProcess.stdout?.pipe(process.stdout); - toolpadProcess.stderr?.pipe(process.stderr); - - const match = await waitForMatch(toolpadProcess.stdout!, /http:\/\/localhost:(\d+)/); - - expect(match).toBeTruthy(); - - const appUrl = match![0]; - const res = await fetch(`${appUrl}/health-check`); - expect(res).toHaveProperty('status', 200); - }, - TEST_TIMEOUT, -); - -test( - 'create-toolpad-app can bootstrap a Toolpad Core app without authentication', - async () => { - testDir = await fs.mkdtemp(path.resolve(os.tmpdir(), './test-app-')); - cp = execa(cliPath, [testDir, '--coreVersion', 'latest'], { - cwd: currentDirectory, - }); - cp.stdout?.pipe(process.stdout); - cp.stderr?.pipe(process.stderr); - - // Wait for the framework prompt and select Next.js (default) - await waitForPromptAndRespond(cp, /Which framework/, '\n'); - - // Wait for the router prompt and select the App Router - await waitForPromptAndRespond(cp, /Which router/, '\n'); - - // Wait for the authentication prompt and select 'No' - await waitForPromptAndRespond(cp, /enable authentication/, 'n'); - - const result = await cp; - expect(result.stdout).toMatch('Run the following to get started'); - const packageJsonContent = await fs.readFile(path.resolve(testDir, './package.json'), { - encoding: 'utf-8', - }); - const packageJson = JSON.parse(packageJsonContent); - expect(packageJson).toEqual( - expect.objectContaining({ - dependencies: expect.objectContaining({ - '@toolpad/core': expect.any(String), - }), - scripts: expect.objectContaining({ - dev: 'next dev', - build: 'next build', - start: 'next start', - lint: 'next lint', - }), - }), - ); - - // check that file exists or not in the directory - const gitignore = await fs.readFile(path.resolve(testDir, './.gitignore'), { - encoding: 'utf-8', - }); - - expect(gitignore.length).toBeGreaterThan(0); - - toolpadProcess = execa('pnpm', ['dev'], { - cwd: testDir, - env: { - FORCE_COLOR: '0', - BROWSER: 'none', - }, - }); - toolpadProcess.stdout?.pipe(process.stdout); - toolpadProcess.stderr?.pipe(process.stderr); - - const match = await waitForMatch(toolpadProcess.stdout!, /http:\/\/localhost:(\d+)/); - - expect(match).toBeTruthy(); - }, - TEST_TIMEOUT, -); - -test( - 'create-toolpad-app can bootstrap a Toolpad Core app with authentication', - async () => { - testDir = await fs.mkdtemp(path.resolve(os.tmpdir(), './test-app-auth-')); - cp = execa(cliPath, [testDir, '--coreVersion', 'latest'], { - cwd: currentDirectory, - }); - - cp.stdout?.pipe(process.stdout); - cp.stderr?.pipe(process.stderr); - - // Wait for the framework prompt and select Next.js (default) - await waitForPromptAndRespond(cp, /Which framework/, '\n'); - - // Wait for the router prompt and select the App Router - await waitForPromptAndRespond(cp, /Which router/, '\n'); - - // Wait for the authentication prompt and select 'Yes' - await waitForPromptAndRespond(cp, /enable authentication/, 'y'); - - // Wait for the auth providers prompt and select all (press 'a' then Enter) - await waitForPromptAndRespond(cp, /Select authentication providers/, 'a\n'); - - const result = await cp; - expect(result.stdout).toMatch('Run the following to get started'); - - const packageJsonContent = await fs.readFile(path.resolve(testDir, './package.json'), { - encoding: 'utf-8', - }); - const packageJson = JSON.parse(packageJsonContent); - expect(packageJson).toEqual( - expect.objectContaining({ - dependencies: expect.objectContaining({ - '@toolpad/core': expect.any(String), - 'next-auth': expect.any(String), - }), - scripts: expect.objectContaining({ - dev: 'next dev', - build: 'next build', - start: 'next start', - lint: 'next lint', - }), - }), - ); - - // Check if auth.ts file is created - const authFileExists = await fs - .access(path.resolve(testDir, './auth.ts')) - .then(() => true) - .catch(() => false); - - expect(authFileExists).toBe(true); - - toolpadProcess = execa('pnpm', ['dev'], { - cwd: testDir, - env: { - FORCE_COLOR: '0', - BROWSER: 'none', - }, - }); - toolpadProcess.stdout?.pipe(process.stdout); - toolpadProcess.stderr?.pipe(process.stderr); - - const match = await waitForMatch(toolpadProcess.stdout!, /http:\/\/localhost:(\d+)/); - - expect(match).toBeTruthy(); - }, - TEST_TIMEOUT, -); - -test( - 'create-toolpad-app can bootstrap a Toolpad Core app with Vite without authentication', - async () => { - testDir = await fs.mkdtemp(path.resolve(os.tmpdir(), './test-app-vite-')); - cp = execa(cliPath, [testDir, '--coreVersion', 'latest'], { - cwd: currentDirectory, - }); - cp.stdout?.pipe(process.stdout); - cp.stderr?.pipe(process.stderr); - - // Wait for the framework prompt and select Vite (down arrow + enter) - await waitForPromptAndRespond(cp, /Which framework/, '\u001b[B'); - - // Wait for the authentication prompt and select 'No' - await waitForPromptAndRespond(cp, /enable authentication/, 'n'); - - const result = await cp; - expect(result.stdout).toMatch('Run the following to get started'); - const packageJsonContent = await fs.readFile(path.resolve(testDir, './package.json'), { - encoding: 'utf-8', - }); - const packageJson = JSON.parse(packageJsonContent); - expect(packageJson).toEqual( - expect.objectContaining({ - dependencies: expect.objectContaining({ - '@toolpad/core': expect.any(String), - }), - scripts: expect.objectContaining({ - dev: 'vite', - preview: 'vite preview', - }), - }), - ); - - // check that file exists or not in the directory - const gitignore = await fs.readFile(path.resolve(testDir, './.gitignore'), { - encoding: 'utf-8', - }); - - expect(gitignore.length).toBeGreaterThan(0); - - toolpadProcess = execa('pnpm', ['dev'], { - cwd: testDir, - env: { - FORCE_COLOR: '0', - BROWSER: 'none', - }, - }); - toolpadProcess.stdout?.pipe(process.stdout); - toolpadProcess.stderr?.pipe(process.stderr); - - // Add console.log to see what output we're getting - toolpadProcess.stdout?.on('data', (data) => { - console.log('Vite output:', data.toString()); - }); - - // Modify the regex to be more lenient - const match = await waitForMatch(toolpadProcess.stdout!, /ready in/); - - expect(match).toBeTruthy(); - }, - TEST_TIMEOUT, -); - -test( - 'create-toolpad-app can bootstrap a Toolpad Core app with Vite with authentication', - async () => { - testDir = await fs.mkdtemp(path.resolve(os.tmpdir(), './test-app-vite-auth-')); - cp = execa(cliPath, [testDir, '--coreVersion', 'latest'], { - cwd: currentDirectory, - }); - cp.stdout?.pipe(process.stdout); - cp.stderr?.pipe(process.stderr); - - // Wait for the framework prompt and select Vite (down arrow + enter) - await waitForPromptAndRespond(cp, /Which framework/, '\u001b[B\n'); - - // Wait for the authentication prompt and select 'Yes' - await waitForPromptAndRespond(cp, /enable authentication/, 'y'); - - // Wait for the auth providers prompt and select all (press 'a' then Enter) - await waitForPromptAndRespond(cp, /Select authentication providers/, 'a\n'); - - const result = await cp; - expect(result.stdout).toMatch('Run the following to get started'); - - const packageJsonContent = await fs.readFile(path.resolve(testDir, './package.json'), { - encoding: 'utf-8', - }); - const packageJson = JSON.parse(packageJsonContent); - expect(packageJson).toEqual( - expect.objectContaining({ - dependencies: expect.objectContaining({ - '@toolpad/core': expect.any(String), - firebase: expect.any(String), - }), - scripts: expect.objectContaining({ - dev: 'vite', - preview: 'vite preview', - }), - }), - ); - - // Check if auth.ts file is created - const authFileExists = await fs - .access(path.resolve(testDir, './src/firebase/auth.ts')) - .then(() => true) - .catch(() => false); - - expect(authFileExists).toBe(true); - - toolpadProcess = execa('pnpm', ['dev'], { - cwd: testDir, - env: { - FORCE_COLOR: '0', - BROWSER: 'none', - }, - }); - toolpadProcess.stdout?.pipe(process.stdout); - toolpadProcess.stderr?.pipe(process.stderr); - - const match = await waitForMatch(toolpadProcess.stdout!, /ready in/); - - expect(match).toBeTruthy(); - }, - TEST_TIMEOUT, -); - -afterEach(async () => { - if (toolpadProcess) { - await terminate(toolpadProcess.pid!); - await toolpadProcess.catch(() => null); - console.log('toolpad ended'); - } - - if (cp) { - await terminate(cp.pid!); - await cp.catch(() => null); - console.log('create-toolpad-app ended'); - } - - if (testDir) { - await fs.rm(testDir, { recursive: true, force: true }); - console.log('test directory cleaned up'); - } -}, 30000); diff --git a/packages/create-toolpad-app/tests/package.json b/packages/create-toolpad-app/tests/package.json deleted file mode 100644 index 3dbc1ca591c..00000000000 --- a/packages/create-toolpad-app/tests/package.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "type": "module" -} diff --git a/packages/create-toolpad-app/tests/tsconfig.json b/packages/create-toolpad-app/tests/tsconfig.json deleted file mode 100644 index 0421216283c..00000000000 --- a/packages/create-toolpad-app/tests/tsconfig.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "../tsconfig.json", - "compilerOptions": { - "module": "ESNext" - } -} diff --git a/packages/create-toolpad-app/tsconfig.json b/packages/create-toolpad-app/tsconfig.json deleted file mode 100644 index a1c769a399d..00000000000 --- a/packages/create-toolpad-app/tsconfig.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./dist/tsconfig.json.tsbuildinfo", - "target": "es2020", - "module": "esnext", - "moduleResolution": "bundler", - "lib": ["esnext"], - "isolatedModules": true, - "strict": true, - "resolveJsonModule": true, - "esModuleInterop": true, - "skipLibCheck": true, - "noEmit": false, - "outDir": "./dist", - "pretty": true, - "rootDir": "src", - "types": ["node"] - }, - "include": ["src/**/*.ts", "src/**/*.tsx"] -} diff --git a/packages/create-toolpad-app/tsup.config.ts b/packages/create-toolpad-app/tsup.config.ts deleted file mode 100644 index b44fc5db1dc..00000000000 --- a/packages/create-toolpad-app/tsup.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { defineConfig } from 'tsup'; - -export default defineConfig({ - entry: ['./src/index.ts'], - silent: true, - noExternal: ['chalk', 'execa', 'inquirer'], - clean: true, - format: 'cjs', - async onSuccess() { - // eslint-disable-next-line no-console - console.log('cli: build successful'); - }, - publicDir: './public', -}); diff --git a/packages/create-toolpad-app/vitest.config.mts b/packages/create-toolpad-app/vitest.config.mts deleted file mode 100644 index bc7bb015e8b..00000000000 --- a/packages/create-toolpad-app/vitest.config.mts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - setupFiles: ['../../test/setupVitest.ts'], - }, -}); diff --git a/packages/toolpad-core/README.md b/packages/toolpad-core/README.md deleted file mode 100644 index 3c4ccafd3bd..00000000000 --- a/packages/toolpad-core/README.md +++ /dev/null @@ -1,20 +0,0 @@ - -

- Toolpad logo -

- -

Toolpad Core

- -Toolpad Core is a set of full-stack components for building dashboards, internal tools, and B2B web applications with React. It is built on top of Next.js and offers a suite of components such as layout, navigation, authentication, and data management interfaces to help you build scalable dashboards fast. - -## Installation - -Install the package in your project directory with: - -```bash -npm install @toolpad/core -``` - -## Documentation - -[The documentation](./docs) diff --git a/packages/toolpad-core/package.json b/packages/toolpad-core/package.json deleted file mode 100644 index 1c4484297d8..00000000000 --- a/packages/toolpad-core/package.json +++ /dev/null @@ -1,102 +0,0 @@ -{ - "name": "@toolpad/core", - "version": "0.13.0", - "author": "Toolpad Team", - "description": "Dashboard framework powered by MUI.", - "main": "./node/index.js", - "module": "./index.js", - "keywords": [ - "react", - "mui", - "toolpad", - "internal tools", - "crud", - "admin", - "dashboard" - ], - "repository": { - "type": "git", - "url": "git+https://github.com/mui/toolpad.git", - "directory": "packages/toolpad-core" - }, - "license": "MIT", - "bugs": { - "url": "https://github.com/mui/toolpad/issues" - }, - "homepage": "https://github.com/mui/toolpad#readme", - "scripts": { - "clean": "rimraf build", - "prebuild": "pnpm clean", - "build": "pnpm build:node && pnpm build:stable && pnpm build:types && pnpm build:copy-files", - "build:node": "node ../../scripts/build.mjs node", - "build:stable": "node ../../scripts/build.mjs stable", - "build:copy-files": "node ../../scripts/copyFiles.mjs", - "build:types": "node ../../scripts/buildTypes.mjs", - "predev": "pnpm clean", - "dev": "mkdir -p build && concurrently \"pnpm build:stable --watch\" \"pnpm build:types --watch\" \"pnpm build:copy-files\"", - "check-types": "pnpm build:stable && pnpm build:types && tsc --noEmit", - "test": "vitest run --coverage", - "test:dev": "vitest", - "test:browser": "vitest run --browser.enabled", - "test:browser:dev": "vitest --browser.enabled" - }, - "dependencies": { - "@babel/runtime": "^7.26.10", - "@mui/utils": "7.0.0-beta.4", - "@standard-schema/spec": "^1.0.0", - "@toolpad/utils": "workspace:*", - "@vitejs/plugin-react": "4.3.4", - "client-only": "^0.0.1", - "dayjs": "1.11.13", - "invariant": "2.2.4", - "path-to-regexp": "6.3.0", - "prop-types": "15.8.1" - }, - "devDependencies": { - "@mui/icons-material": "6.4.7", - "@mui/material": "6.4.7", - "@mui/x-data-grid": "7.28.0", - "@mui/x-data-grid-premium": "7.28.0", - "@mui/x-data-grid-pro": "7.28.0", - "@mui/x-date-pickers": "7.28.0", - "@types/invariant": "2.2.37", - "@types/prop-types": "15.7.14", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "@types/sinon": "^17.0.4", - "@vitest/browser": "2.1.9", - "next": "^15.2.3", - "next-router-mock": "^0.9.13", - "playwright": "^1.47.2", - "react-router": "7.1.5", - "sinon": "^19.0.2", - "vitest": "2.1.9" - }, - "peerDependencies": { - "@mui/icons-material": "^6.4.0 || ^7.0.0 || ^7.0.0-beta", - "@mui/material": "^6.4.0 || ^7.0.0 || ^7.0.0-beta", - "@mui/x-data-grid": "^7", - "@mui/x-data-grid-premium": "^7", - "@mui/x-data-grid-pro": "^7", - "@mui/x-date-pickers": "^7", - "next": "^14 || ^15", - "react": "^18 || ^19", - "react-router": "^7" - }, - "peerDependenciesMeta": { - "next": { - "optional": true - }, - "react-router": { - "optional": true - } - }, - "sideEffects": false, - "publishConfig": { - "access": "public", - "directory": "build" - }, - "engines": { - "node": ">=14.0.0" - } -} diff --git a/packages/toolpad-core/src/Account/Account.test.tsx b/packages/toolpad-core/src/Account/Account.test.tsx deleted file mode 100644 index a94e4e4c4d2..00000000000 --- a/packages/toolpad-core/src/Account/Account.test.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/// - -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import describeConformance from '@toolpad/utils/describeConformance'; -import { Account } from './Account'; -import { AppProvider } from '../AppProvider'; - -describe('AppProvider', () => { - describeConformance(, () => ({ - skip: ['themeDefaultProps'], - })); - - test('renders nothing in button when no authentication', async () => { - render(); - - expect(screen.queryByRole('button')).not.toBeInTheDocument(); - }); - - test('renders log in button when no session', async () => { - const auth = { signIn: vi.fn(), signOut: vi.fn() }; - render( - - - , - ); - - const loginButton = screen.getByRole('button', { name: 'Sign In' }); - - await userEvent.click(loginButton); - - expect(auth.signIn).toHaveBeenCalled(); - }); - - test('renders content correctly when there is a session', async () => { - const auth = { signIn: vi.fn(), signOut: vi.fn() }; - const session = { user: { name: 'John Doe', email: 'john@example.com' } }; - render( - - - , - ); - - const userButton = screen.getByRole('button', { name: 'Current User' }); - - await userEvent.click(userButton); - - expect(screen.getByText('John Doe', { selector: 'p' })).toBeInTheDocument(); - expect(screen.getByText('john@example.com')).toBeInTheDocument(); - - const signOutButton = screen.getByRole('button', { name: 'Sign Out' }); - - await userEvent.click(signOutButton); - - expect(auth.signOut).toHaveBeenCalled(); - }); -}); diff --git a/packages/toolpad-core/src/Account/Account.tsx b/packages/toolpad-core/src/Account/Account.tsx deleted file mode 100644 index 90966967051..00000000000 --- a/packages/toolpad-core/src/Account/Account.tsx +++ /dev/null @@ -1,257 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Button, { ButtonProps } from '@mui/material/Button'; -import Popover, { PopoverProps } from '@mui/material/Popover'; -import Divider from '@mui/material/Divider'; -import Stack, { StackProps } from '@mui/material/Stack'; -import { SignInButton } from './SignInButton'; -import { SignOutButton } from './SignOutButton'; -import { AccountPreview, AccountPreviewProps } from './AccountPreview'; -import { AccountPopoverHeader } from './AccountPopoverHeader'; -import { AccountPopoverFooter } from './AccountPopoverFooter'; -import { SessionContext, AuthenticationContext } from '../AppProvider/AppProvider'; -import { useLocaleText, type LocaleText } from '../AppProvider/LocalizationProvider'; -import { AccountLocaleContext } from './AccountLocaleContext'; - -interface AccountLocaleText { - accountSignInLabel: string; - accountSignOutLabel: string; - - accountPreviewIconButtonLabel: string; - accountPreviewTitle: string; -} - -export interface AccountSlots { - /** - * The component used for the account preview - * @default AccountPreview - */ - preview?: React.JSXElementConstructor; - /** - * The component used for the account popover menu - * @default Popover - */ - popover?: React.JSXElementConstructor; - /** - * The component used for the content of account popover - * @default Stack - */ - popoverContent?: React.JSXElementConstructor; - /** - * The component used for the sign in button. - * @default Button - */ - signInButton?: React.JSXElementConstructor; - /** - * The component used for the sign out button. - * @default Button - */ - signOutButton?: React.JSXElementConstructor; -} - -export interface AccountProps { - /** - * The components used for each slot inside. - */ - slots?: AccountSlots; - /** - * The props used for each slot inside. - */ - slotProps?: { - preview?: AccountPreviewProps; - popover?: Omit, 'open'>; - popoverContent?: React.ComponentProps; - signInButton?: React.ComponentProps; - signOutButton?: React.ComponentProps; - }; - /** - * The labels for the account component. - */ - localeText?: Partial; -} - -const defaultAccountLocaleText: Pick = { - accountPreviewIconButtonLabel: 'Current User', - accountPreviewTitle: 'Account', - accountSignInLabel: 'Sign in', - accountSignOutLabel: 'Sign out', -}; - -/** - * - * Demos: - * - * - [Account](https://mui.com/toolpad/core/react-account/) - * - [Dashboard Layout](https://mui.com/toolpad/core/react-dashboard-layout/) - * - [Sign-in Page](https://mui.com/toolpad/core/react-sign-in-page/) - * - * API: - * - * - [Account API](https://mui.com/toolpad/core/api/account) - */ -function Account(props: AccountProps) { - const { localeText: propsLocaleText } = props; - const globalLocaleText = useLocaleText(); - const localeText = React.useMemo( - () => ({ ...defaultAccountLocaleText, ...globalLocaleText, ...propsLocaleText }), - [globalLocaleText, propsLocaleText], - ); - const { slots, slotProps } = props; - const [anchorEl, setAnchorEl] = React.useState(null); - const session = React.useContext(SessionContext); - const authentication = React.useContext(AuthenticationContext); - const open = Boolean(anchorEl); - - const handleClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleClose = () => { - setAnchorEl(null); - }; - - if (!authentication) { - return null; - } - - let accountContent = null; - - if (!session?.user) { - accountContent = slots?.signInButton ? ( - - ) : ( - - ); - } else { - accountContent = ( - - {slots?.preview ? ( - - ) : ( - - )} - {slots?.popover ? ( - - ) : ( - - `drop-shadow(0px 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.32)'})`, - mt: 1, - '&::before': { - content: '""', - display: 'block', - position: 'absolute', - top: 0, - right: 14, - width: 10, - height: 10, - bgcolor: 'background.paper', - transform: 'translateY(-50%) rotate(45deg)', - zIndex: 0, - }, - }, - }, - ...slotProps?.popover?.slotProps, - }} - > - {slots?.popoverContent ? ( - - ) : ( - - - - - - - - - - )} - - )} - - ); - } - - return ( - - {accountContent} - - ); -} - -Account.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * The labels for the account component. - */ - localeText: PropTypes.object, - /** - * The props used for each slot inside. - */ - slotProps: PropTypes.shape({ - popover: PropTypes.object, - popoverContent: PropTypes.object, - preview: PropTypes.shape({ - handleClick: PropTypes.func, - open: PropTypes.bool, - slotProps: PropTypes.shape({ - avatar: PropTypes.object, - avatarIconButton: PropTypes.object, - moreIconButton: PropTypes.object, - }), - slots: PropTypes.shape({ - avatar: PropTypes.elementType, - avatarIconButton: PropTypes.elementType, - moreIconButton: PropTypes.elementType, - }), - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), - variant: PropTypes.oneOf(['condensed', 'expanded']), - }), - signInButton: PropTypes.object, - signOutButton: PropTypes.object, - }), - /** - * The components used for each slot inside. - */ - slots: PropTypes.shape({ - popover: PropTypes.elementType, - popoverContent: PropTypes.elementType, - preview: PropTypes.elementType, - signInButton: PropTypes.elementType, - signOutButton: PropTypes.elementType, - }), -} as any; - -export { Account }; diff --git a/packages/toolpad-core/src/Account/AccountLocaleContext.tsx b/packages/toolpad-core/src/Account/AccountLocaleContext.tsx deleted file mode 100644 index eb0357adb8e..00000000000 --- a/packages/toolpad-core/src/Account/AccountLocaleContext.tsx +++ /dev/null @@ -1,8 +0,0 @@ -'use client'; -import * as React from 'react'; -import type { LocaleText } from '../AppProvider/LocalizationProvider'; - -/** - * @ignore - internal component. - */ -export const AccountLocaleContext = React.createContext | null>(null); diff --git a/packages/toolpad-core/src/Account/AccountPopoverFooter.tsx b/packages/toolpad-core/src/Account/AccountPopoverFooter.tsx deleted file mode 100644 index 01699d5a0a3..00000000000 --- a/packages/toolpad-core/src/Account/AccountPopoverFooter.tsx +++ /dev/null @@ -1,50 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Box, { BoxProps } from '@mui/material/Box'; - -export interface AccountPopoverFooterProps extends BoxProps { - children?: React.ReactNode; -} - -/** - * - * Demos: - * - * - [Account](https://mui.com/toolpad/core/react-account/) - * - * API: - * - * - [AccountPopoverFooter API](https://mui.com/toolpad/core/api/account-popover-footer) - */ -function AccountPopoverFooter(props: AccountPopoverFooterProps) { - const { children, ...rest } = props; - return ( - - {children} - - ); -} - -AccountPopoverFooter.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * @ignore - */ - children: PropTypes.node, - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), -} as any; - -export { AccountPopoverFooter }; diff --git a/packages/toolpad-core/src/Account/AccountPopoverHeader.tsx b/packages/toolpad-core/src/Account/AccountPopoverHeader.tsx deleted file mode 100644 index 10de9d409f6..00000000000 --- a/packages/toolpad-core/src/Account/AccountPopoverHeader.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Stack, { StackProps } from '@mui/material/Stack'; - -export interface AccountPopoverHeaderProps extends StackProps { - children?: React.ReactNode; -} - -/** - * - * Demos: - * - * - [Account](https://mui.com/toolpad/core/react-account/) - * - * API: - * - * - [AccountPopoverHeader API](https://mui.com/toolpad/core/api/account-popover-header) - */ -function AccountPopoverHeader(props: AccountPopoverHeaderProps) { - const { children, ...rest } = props; - return {children}; -} - -AccountPopoverHeader.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * The content of the component. - */ - children: PropTypes.node, -} as any; - -export { AccountPopoverHeader }; diff --git a/packages/toolpad-core/src/Account/AccountPreview.test.tsx b/packages/toolpad-core/src/Account/AccountPreview.test.tsx deleted file mode 100644 index 9024d9cef3a..00000000000 --- a/packages/toolpad-core/src/Account/AccountPreview.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/// - -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { AccountPreview } from './AccountPreview'; -import { AppProvider } from '../AppProvider'; - -describe('AccountPreview', () => { - const auth = { signIn: vi.fn(), signOut: vi.fn() }; - const session = { - user: { name: 'John Doe', email: 'john@example.com', image: 'https://example.com/avatar.jpg' }, - }; - - test('renders nothing when no session is provided', () => { - render( - - - , - ); - - expect(screen.queryByRole('button')).not.toBeInTheDocument(); - }); - - test('displays condensed variant by default', () => { - render( - - - , - ); - - const avatar = screen.getByRole('img', { name: 'John Doe' }); - expect(avatar).toBeInTheDocument(); - expect(screen.queryByText('John Doe')).not.toBeInTheDocument(); - expect(avatar).toHaveAttribute('src', 'https://example.com/avatar.jpg'); - }); - - test('displays user name, email, and avatar in expanded variant', () => { - render( - - - , - ); - - expect(screen.getByText('John Doe')).toBeInTheDocument(); - expect(screen.getByText('john@example.com')).toBeInTheDocument(); - expect(screen.getByRole('img', { name: 'John Doe' })).toBeInTheDocument(); - }); - - test('calls handleClick when more icon button is clicked in expanded variant', async () => { - const handleClick = vi.fn(); - render( - - - , - ); - - const moreButton = screen.getByRole('button'); - await userEvent.click(moreButton); - - expect(handleClick).toHaveBeenCalled(); - }); - - test('calls handleClick when avatar is clicked in condensed variant', async () => { - const handleClick = vi.fn(); - render( - - - , - ); - - const avatarButton = screen.getByRole('button', { name: 'Current User' }); - await userEvent.click(avatarButton); - - expect(handleClick).toHaveBeenCalled(); - }); -}); diff --git a/packages/toolpad-core/src/Account/AccountPreview.tsx b/packages/toolpad-core/src/Account/AccountPreview.tsx deleted file mode 100644 index a5f41386680..00000000000 --- a/packages/toolpad-core/src/Account/AccountPreview.tsx +++ /dev/null @@ -1,204 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Avatar, { AvatarProps } from '@mui/material/Avatar'; -import { SxProps } from '@mui/material/styles'; -import Typography from '@mui/material/Typography'; -import Tooltip from '@mui/material/Tooltip'; -import Stack from '@mui/material/Stack'; -import IconButton, { IconButtonProps } from '@mui/material/IconButton'; -import MoreVertIcon from '@mui/icons-material/MoreVert'; -import { SessionContext } from '../AppProvider'; -import { useLocaleText } from '../AppProvider/LocalizationProvider'; -import { AccountLocaleContext } from './AccountLocaleContext'; - -export type AccountPreviewVariant = 'condensed' | 'expanded'; - -export interface AccountPreviewSlots { - /** - * The component used for the Avatar - * @default Avatar - */ - avatar?: React.ElementType; - /** - * The component used for the overflow icon button in the expanded variant - * @default IconButton - */ - moreIconButton?: React.ElementType; - /** - * The component used for the avatar icon button in the condensed variant - * @default IconButton - */ - avatarIconButton?: React.ElementType; -} - -export interface AccountPreviewProps { - /** - * The components used for each slot inside. - */ - slots?: AccountPreviewSlots; - /** - * The props used for each slot inside. - */ - slotProps?: { - avatar?: AvatarProps; - moreIconButton?: IconButtonProps; - avatarIconButton?: IconButtonProps; - }; - /** - * The type of account details to display. - * @property {'condensed'} condensed - Shows only the user's avatar. - * @property {'expanded'} expanded - Displays the user's avatar, name, and email if available. - * @default 'condensed' - */ - variant?: AccountPreviewVariant; - /** - * The handler used when the preview is expanded - */ - handleClick?: React.MouseEventHandler; - /** - * The state of the Account popover - * @default false - */ - open?: boolean; - /** - * The prop used to customize the styling of the preview - */ - sx?: SxProps; -} - -/** - * The AccountPreview component displays user account information. - * - * Demos: - * - * - [Account](https://mui.com/toolpad/core/react-account/) - * - * API: - * - * - [AccountPreview API](https://mui.com/toolpad/core/api/account-preview) - */ -function AccountPreview(props: AccountPreviewProps) { - const { slots, variant = 'condensed', slotProps, open, handleClick, sx } = props; - const session = React.useContext(SessionContext); - const globalLocaleText = useLocaleText(); - const accountLocaleText = React.useContext(AccountLocaleContext); - const localeText = { ...globalLocaleText, ...accountLocaleText }; - - if (!session || !session.user) { - return null; - } - - const avatarContent = slots?.avatar ? ( - - ) : ( - - ); - - if (variant === 'expanded') { - return ( - - - {avatarContent} - - - {session.user?.name} - - - {session.user?.email} - - - - {handleClick && - (slots?.moreIconButton ? ( - - ) : ( - - - - ))} - - ); - } - - return ( - - {slots?.avatarIconButton ? ( - - ) : ( - - - {avatarContent} - - - )} - - ); -} - -AccountPreview.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * The handler used when the preview is expanded - */ - handleClick: PropTypes.func, - /** - * The state of the Account popover - * @default false - */ - open: PropTypes.bool, - /** - * The props used for each slot inside. - */ - slotProps: PropTypes.shape({ - avatar: PropTypes.object, - avatarIconButton: PropTypes.object, - moreIconButton: PropTypes.object, - }), - /** - * The components used for each slot inside. - */ - slots: PropTypes.shape({ - avatar: PropTypes.elementType, - avatarIconButton: PropTypes.elementType, - moreIconButton: PropTypes.elementType, - }), - /** - * The prop used to customize the styling of the preview - */ - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), - /** - * The type of account details to display. - * @property {'condensed'} condensed - Shows only the user's avatar. - * @property {'expanded'} expanded - Displays the user's avatar, name, and email if available. - * @default 'condensed' - */ - variant: PropTypes.oneOf(['condensed', 'expanded']), -} as any; - -export { AccountPreview }; diff --git a/packages/toolpad-core/src/Account/SignInButton.tsx b/packages/toolpad-core/src/Account/SignInButton.tsx deleted file mode 100644 index 6da6006112c..00000000000 --- a/packages/toolpad-core/src/Account/SignInButton.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Button, { ButtonProps } from '@mui/material/Button'; -import { AuthenticationContext } from '../AppProvider'; -import { useLocaleText } from '../AppProvider/LocalizationProvider'; -import { AccountLocaleContext } from './AccountLocaleContext'; - -/** - * - * Demos: - * - * - [Account](https://mui.com/toolpad/core/react-account/) - * - * API: - * - * - [SignInButton API](https://mui.com/toolpad/core/api/sign-in-button) - */ -function SignInButton(props: ButtonProps) { - const authentication = React.useContext(AuthenticationContext); - const globalLocaleText = useLocaleText(); - const accountLocaleText = React.useContext(AccountLocaleContext); - const localeText = { ...globalLocaleText, ...accountLocaleText }; - - return ( - - ); -} - -SignInButton.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * The content of the component. - */ - children: PropTypes.node, -} as any; - -export { SignInButton }; diff --git a/packages/toolpad-core/src/Account/SignOutButton.tsx b/packages/toolpad-core/src/Account/SignOutButton.tsx deleted file mode 100644 index 45add1b6fec..00000000000 --- a/packages/toolpad-core/src/Account/SignOutButton.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Button, { ButtonProps } from '@mui/material/Button'; -import LogoutIcon from '@mui/icons-material/Logout'; -import { AuthenticationContext } from '../AppProvider'; -import { useLocaleText } from '../AppProvider/LocalizationProvider'; -import { AccountLocaleContext } from './AccountLocaleContext'; - -export type SignOutButtonProps = ButtonProps; - -/** - * - * Demos: - * - * - [Account](https://mui.com/toolpad/core/react-account/) - * - * API: - * - * - [SignOutButton API](https://mui.com/toolpad/core/api/sign-out-button) - */ -function SignOutButton(props: SignOutButtonProps) { - const authentication = React.useContext(AuthenticationContext); - const globalLocaleText = useLocaleText(); - const accountLocaleText = React.useContext(AccountLocaleContext); - const localeText = { ...globalLocaleText, ...accountLocaleText }; - - return ( - - ); -} - -SignOutButton.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * The content of the component. - */ - children: PropTypes.node, -} as any; - -export { SignOutButton }; diff --git a/packages/toolpad-core/src/Account/index.ts b/packages/toolpad-core/src/Account/index.ts deleted file mode 100644 index cb01e597927..00000000000 --- a/packages/toolpad-core/src/Account/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from './Account'; -export * from './AccountPreview'; -export * from './AccountPopoverHeader'; -export * from './AccountPopoverFooter'; -export * from './SignOutButton'; -export * from './SignInButton'; diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx deleted file mode 100644 index 44062bd5490..00000000000 --- a/packages/toolpad-core/src/AppProvider/AppProvider.test.tsx +++ /dev/null @@ -1,25 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test, expect } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import { createTheme } from '@mui/material/styles'; -import { AppProvider } from './AppProvider'; - -describe('AppProvider', () => { - test('renders content correctly', async () => { - render(Hello world); - - expect(screen.getByText('Hello world')).toBeTruthy(); - }); - - test('renders content correctly when using legacy theme', async () => { - const legacyTheme = createTheme(); - - render(Hello world); - - expect(screen.getByText('Hello world')).toBeTruthy(); - }); -}); diff --git a/packages/toolpad-core/src/AppProvider/AppProvider.tsx b/packages/toolpad-core/src/AppProvider/AppProvider.tsx deleted file mode 100644 index e56530907f0..00000000000 --- a/packages/toolpad-core/src/AppProvider/AppProvider.tsx +++ /dev/null @@ -1,289 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { createTheme as createMuiTheme, Theme } from '@mui/material/styles'; -import { NotificationsProvider } from '../useNotifications'; -import { DialogsProvider } from '../useDialogs'; -import { - BrandingContext, - NavigationContext, - RouterContext, - WindowContext, -} from '../shared/context'; -import type { LinkProps } from '../shared/Link'; -import { AppThemeProvider } from './AppThemeProvider'; -import { LocalizationProvider, type LocaleText } from './LocalizationProvider'; - -export interface NavigateOptions { - history?: 'auto' | 'push' | 'replace'; -} - -export interface Navigate { - (url: string | URL, options?: NavigateOptions): void; -} - -/** - * Abstract router used by Toolpad components. - */ -export interface Router { - pathname: string; - searchParams: URLSearchParams; - navigate: Navigate; - Link?: React.ComponentType; -} - -export interface Branding { - title?: string; - logo?: React.ReactNode; - homeUrl?: string; -} - -export interface NavigationPageItem { - kind?: 'page'; - segment?: string; - title?: string; - icon?: React.ReactNode; - pattern?: string; - action?: React.ReactNode; - children?: Navigation; -} - -export interface NavigationSubheaderItem { - kind: 'header'; - title: string; -} - -export interface NavigationDividerItem { - kind: 'divider'; -} - -export type NavigationItem = NavigationPageItem | NavigationSubheaderItem | NavigationDividerItem; - -export type Navigation = NavigationItem[]; - -export interface Session { - user?: { - id?: string | null; - name?: string | null; - image?: string | null; - email?: string | null; - }; -} - -export interface Authentication { - signIn: () => void; - signOut: () => void; -} - -export const AuthenticationContext = React.createContext(null); - -export const SessionContext = React.createContext(null); - -export type AppTheme = Theme | { light: Theme; dark: Theme }; - -export interface AppProviderProps { - /** - * The content of the app provider. - */ - children: React.ReactNode; - /** - * [Theme or themes](https://mui.com/toolpad/core/react-app-provider/#theming) to be used by the app in light/dark mode. A [CSS variables theme](https://mui.com/material-ui/customization/css-theme-variables/overview/) is recommended. - * @default createTheme() - */ - theme?: AppTheme; - /** - * Branding options for the app. - * @default null - */ - branding?: Branding | null; - /** - * Navigation definition for the app. [Find out more](https://mui.com/toolpad/core/react-app-provider/#navigation). - * @default [] - */ - navigation?: Navigation; - /** - * Router implementation used inside Toolpad components. - * @default null - */ - router?: Router; - /** - * Locale text for components - */ - localeText?: Partial; - /** - * Session info about the current user. - * @default null - */ - session?: Session | null; - /** - * Authentication methods. - * @default null - */ - authentication?: Authentication | null; - /** - * The window where the application is rendered. - * This is needed when rendering the app inside an iframe, for example. - * @default window - */ - window?: Window; -} - -function createTheme(): Theme { - return createMuiTheme({ - cssVariables: { - colorSchemeSelector: 'data-toolpad-color-scheme', - }, - colorSchemes: { dark: true }, - }); -} - -/** - * - * Demos: - * - * - [App Provider](https://mui.com/toolpad/core/react-app-provider/) - * - [Dashboard Layout](https://mui.com/toolpad/core/react-dashboard-layout/) - * - * API: - * - * - [AppProvider API](https://mui.com/toolpad/core/api/app-provider) - */ -function AppProvider(props: AppProviderProps) { - const { - children, - theme = createTheme(), - branding = null, - navigation = [], - localeText, - router = null, - authentication = null, - session = null, - window: appWindow, - } = props; - - return ( - - - - - - - - - - - {children} - - - - - - - - - - - ); -} - -AppProvider.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Authentication methods. - * @default null - */ - authentication: PropTypes.shape({ - signIn: PropTypes.func.isRequired, - signOut: PropTypes.func.isRequired, - }), - /** - * Branding options for the app. - * @default null - */ - branding: PropTypes.shape({ - homeUrl: PropTypes.string, - logo: PropTypes.node, - title: PropTypes.string, - }), - /** - * The content of the app provider. - */ - children: PropTypes.node, - /** - * Locale text for components - */ - localeText: PropTypes.object, - /** - * Navigation definition for the app. [Find out more](https://mui.com/toolpad/core/react-app-provider/#navigation). - * @default [] - */ - navigation: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.shape({ - action: PropTypes.node, - children: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.object, - PropTypes.shape({ - kind: PropTypes.oneOf(['header']).isRequired, - title: PropTypes.string.isRequired, - }), - PropTypes.shape({ - kind: PropTypes.oneOf(['divider']).isRequired, - }), - ]).isRequired, - ), - icon: PropTypes.node, - kind: PropTypes.oneOf(['page']), - pattern: PropTypes.string, - segment: PropTypes.string, - title: PropTypes.string, - }), - PropTypes.shape({ - kind: PropTypes.oneOf(['header']).isRequired, - title: PropTypes.string.isRequired, - }), - PropTypes.shape({ - kind: PropTypes.oneOf(['divider']).isRequired, - }), - ]).isRequired, - ), - /** - * Router implementation used inside Toolpad components. - * @default null - */ - router: PropTypes.shape({ - Link: PropTypes.func, - navigate: PropTypes.func.isRequired, - pathname: PropTypes.string.isRequired, - searchParams: PropTypes.instanceOf(URLSearchParams).isRequired, - }), - /** - * Session info about the current user. - * @default null - */ - session: PropTypes.shape({ - user: PropTypes.shape({ - email: PropTypes.string, - id: PropTypes.string, - image: PropTypes.string, - name: PropTypes.string, - }), - }), - /** - * [Theme or themes](https://mui.com/toolpad/core/react-app-provider/#theming) to be used by the app in light/dark mode. A [CSS variables theme](https://mui.com/material-ui/customization/css-theme-variables/overview/) is recommended. - * @default createTheme() - */ - theme: PropTypes.object, - /** - * The window where the application is rendered. - * This is needed when rendering the app inside an iframe, for example. - * @default window - */ - window: PropTypes.object, -} as any; - -export { AppProvider }; diff --git a/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx b/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx deleted file mode 100644 index 444fcb889d1..00000000000 --- a/packages/toolpad-core/src/AppProvider/AppThemeProvider.tsx +++ /dev/null @@ -1,167 +0,0 @@ -import * as React from 'react'; -import { PaletteMode, Theme, useMediaQuery } from '@mui/material'; -import { ThemeProvider, useColorScheme } from '@mui/material/styles'; -import InitColorSchemeScript from '@mui/material/InitColorSchemeScript'; -import CssBaseline from '@mui/material/CssBaseline'; -import invariant from 'invariant'; -import { useLocalStorageState } from '../useLocalStorageState'; -import { PaletteModeContext } from '../shared/context'; -import type { AppTheme } from './AppProvider'; - -const COLOR_SCHEME_ATTRIBUTE = 'data-toolpad-color-scheme'; -const COLOR_SCHEME_STORAGE_KEY = 'toolpad-color-scheme'; -const MODE_STORAGE_KEY = 'toolpad-mode'; - -function usePreferredMode(window?: Window) { - const prefersDarkMode = useMediaQuery( - '(prefers-color-scheme: dark)', - window && { - matchMedia: window.matchMedia, - }, - ); - return prefersDarkMode ? 'dark' : 'light'; -} - -type ThemeMode = PaletteMode | 'system'; - -type CssVarsTheme = Theme & { vars: Record }; - -function isCssVarsTheme(theme: AppTheme): theme is CssVarsTheme { - return 'vars' in theme; -} - -interface LegacyThemeProviderProps { - children: React.ReactNode; - theme: AppTheme; - window?: Window; -} - -/** - * Compatibility layer for classic v5 themes. It will handle state management for the theme switcher. - * In the v6 theme, this state management is handled by `useColorScheme`. But this hook will crash if - * not run under context with a css vars theme. - */ -function LegacyThemeProvider(props: LegacyThemeProviderProps) { - const { children, theme, window: appWindow } = props; - invariant(!isCssVarsTheme(theme), 'This provider only accepts legacy themes.'); - - const isDualTheme = 'light' in theme || 'dark' in theme; - - const preferredMode = usePreferredMode(appWindow); - const [userMode, setUserMode] = useLocalStorageState(MODE_STORAGE_KEY, 'system'); - - const paletteMode = !userMode || userMode === 'system' ? preferredMode : userMode; - const dualAwareTheme = React.useMemo( - () => - isDualTheme - ? (theme[paletteMode === 'dark' ? 'dark' : 'light'] ?? - theme[paletteMode === 'dark' ? 'light' : 'dark']) - : theme, - [isDualTheme, paletteMode, theme], - ); - - // The v5 shim, based on local state - const paletteModeContextValue = React.useMemo( - () => ({ - paletteMode, - setPaletteMode: setUserMode, - isDualTheme, - }), - [isDualTheme, paletteMode, setUserMode], - ); - - return ( - - - - {children} - - - ); -} - -interface CssVarsPaletteModeProviderProps { - children: React.ReactNode; - window?: Window; -} - -function CssVarsPaletteModeProvider(props: CssVarsPaletteModeProviderProps) { - const { children, window: appWindow } = props; - - const preferredMode = usePreferredMode(appWindow); - const { mode, setMode, allColorSchemes } = useColorScheme(); - - // The v6 API, based on `useColorScheme` - const paletteModeContextValue = React.useMemo(() => { - return { - paletteMode: !mode || mode === 'system' ? preferredMode : mode, - setPaletteMode: setMode, - isDualTheme: allColorSchemes.length > 1, - }; - }, [allColorSchemes, mode, preferredMode, setMode]); - - return ( - - {children} - - ); -} - -interface CssVarsThemeProviderProps { - children: React.ReactNode; - theme: Theme; - window?: Window; -} - -function CssVarsThemeProvider(props: CssVarsThemeProviderProps) { - const { children, theme, window: appWindow } = props; - invariant(isCssVarsTheme(theme), 'This provider only accepts CSS vars themes.'); - - return ( - - - - - {children} - - - ); -} - -interface AppThemeProviderProps { - children: React.ReactNode; - theme: AppTheme; - window?: Window; -} - -/** - * @ignore - internal component. - */ -function AppThemeProvider(props: AppThemeProviderProps) { - const { children, theme, ...rest } = props; - - const useCssVarsProvider = isCssVarsTheme(theme); - - return useCssVarsProvider ? ( - - {children} - - ) : ( - - {children} - - ); -} - -export { AppThemeProvider }; diff --git a/packages/toolpad-core/src/AppProvider/LocalizationProvider.tsx b/packages/toolpad-core/src/AppProvider/LocalizationProvider.tsx deleted file mode 100644 index ab3c07de38c..00000000000 --- a/packages/toolpad-core/src/AppProvider/LocalizationProvider.tsx +++ /dev/null @@ -1,124 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { useTheme } from '@mui/material/styles'; -import DEFAULT_LOCALE from '../locales/en'; - -export interface LocaleText { - // Account - accountSignInLabel: string; - accountSignOutLabel: string; - - // AccountPreview - accountPreviewIconButtonLabel: string; - accountPreviewTitle: string; - - // SignInPage - signInTitle: string; - signInSubtitle: string; - oauthSignInTitle: string; - passkeySignInTitle: string; - magicLinkSignInTitle: string; - signInRememberMe: string; - - // Common authentication labels - email: string; - passkey: string; - username: string; - password: string; - - // Common action labels - or: string; - to: string; - with: string; - save: string; - cancel: string; - ok: string; - close: string; - delete: string; - alert: string; - confirm: string; - loading: string; - - // CRUD - createNewButtonLabel: string; - reloadButtonLabel: string; - createLabel: string; - createSuccessMessage: string; - createErrorMessage: string; - editLabel: string; - editSuccessMessage: string; - editErrorMessage: string; - deleteLabel: string; - deleteConfirmTitle: string; - deleteConfirmMessage: string; - deleteConfirmLabel: string; - deleteCancelLabel: string; - deleteSuccessMessage: string; - deleteErrorMessage: string; - deletedItemMessage: string; -} - -export interface LocalizationProviderProps { - children?: React.ReactNode; - /** - * Locale for components texts - */ - localeText?: Partial; -} - -export const LocalizationContext = React.createContext>({}); - -const LocalizationProvider = function LocalizationProvider(props: LocalizationProviderProps) { - const { localeText: propsLocaleText, children } = props; - - const theme = useTheme(); - // @ts-ignore - const themeLocaleText = theme?.components?.MuiLocalizationProvider?.defaultProps?.localeText; - - const defaultLocaleText = - DEFAULT_LOCALE.components.MuiLocalizationProvider.defaultProps.localeText; - - /* The order of overrides is: - * 1. The `localeText` prop of the `AppProvider` supersedes - * 2. The localeText provided as an argument to the `createTheme` function, which supersedes - * 3. The default locale text - */ - - const localeText = React.useMemo( - () => ({ ...defaultLocaleText, ...themeLocaleText, ...propsLocaleText }), - [defaultLocaleText, themeLocaleText, propsLocaleText], - ); - - return {children}; -}; - -LocalizationProvider.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * @ignore - */ - children: PropTypes.node, - /** - * Locale for components texts - */ - localeText: PropTypes.object, -} as any; - -export { LocalizationProvider }; -/** - * - * Demos: - * - * - [Sign-in Page](https://mui.com/toolpad/core/react-sign-in-page/) - * - * API: - * - * - [LocalizationProvider API](https://mui.com/toolpad/core/api/localization-provider) - */ -export function useLocaleText() { - return React.useContext(LocalizationContext); -} diff --git a/packages/toolpad-core/src/AppProvider/index.ts b/packages/toolpad-core/src/AppProvider/index.ts deleted file mode 100644 index 007fe400d13..00000000000 --- a/packages/toolpad-core/src/AppProvider/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './AppProvider'; -export * from './LocalizationProvider'; diff --git a/packages/toolpad-core/src/Crud/Create.tsx b/packages/toolpad-core/src/Crud/Create.tsx deleted file mode 100644 index 86eda687a78..00000000000 --- a/packages/toolpad-core/src/Crud/Create.tsx +++ /dev/null @@ -1,237 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import invariant from 'invariant'; -import { useNotifications } from '../useNotifications'; -import { CrudContext } from '../shared/context'; -import { useLocaleText } from '../AppProvider/LocalizationProvider'; -import { CrudForm } from './CrudForm'; -import { DataSourceCache } from './cache'; -import { useCachedDataSource } from './useCachedDataSource'; -import { CRUD_DEFAULT_LOCALE_TEXT, type CRUDLocaleText } from './localeText'; -import type { DataModel, DataSource, OmitId } from './types'; - -export interface CreateProps { - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource?: DataSource & Required, 'createOne'>>; - /** - * Initial form values. - * @default {} - */ - initialValues?: Partial>; - /** - * Callback fired when the form is successfully submitted. - */ - onSubmitSuccess?: (formValues: Partial>) => void | Promise; - /** - * Whether the form fields should reset after the form is submitted. - * @default false - */ - resetOnSubmit?: boolean; - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache?: DataSourceCache | null; - /** - * Locale text for the component. - */ - localeText?: CRUDLocaleText; -} - -/** - * - * Demos: - * - * - [CRUD](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [Create API](https://mui.com/toolpad/core/api/create) - */ -function Create(props: CreateProps) { - const { - initialValues = {} as Partial>, - onSubmitSuccess, - resetOnSubmit = false, - dataSourceCache, - localeText: propsLocaleText, - } = props; - - const globalLocaleText = useLocaleText(); - const localeText = { ...CRUD_DEFAULT_LOCALE_TEXT, ...globalLocaleText, ...propsLocaleText }; - - const crudContext = React.useContext(CrudContext); - const dataSource = (props.dataSource ?? crudContext.dataSource) as NonNullable< - typeof props.dataSource - >; - - const notifications = useNotifications(); - - invariant(dataSource, 'No data source found.'); - - const cache = React.useMemo(() => { - const manualCache = dataSourceCache ?? crudContext.dataSourceCache; - return typeof manualCache !== 'undefined' ? manualCache : new DataSourceCache(); - }, [crudContext.dataSourceCache, dataSourceCache]); - const cachedDataSource = useCachedDataSource(dataSource, cache) as NonNullable< - typeof props.dataSource - >; - - const { fields, createOne, validate } = cachedDataSource; - - const [formState, setFormState] = React.useState<{ - values: Partial>; - errors: Partial>; - }>(() => ({ - values: { - ...Object.fromEntries( - fields - .filter(({ field }) => field !== 'id') - .map(({ field, type }) => [ - field, - type === 'boolean' ? (initialValues[field] ?? false) : initialValues[field], - ]), - ), - ...initialValues, - }, - errors: {}, - })); - const formValues = formState.values; - const formErrors = formState.errors; - - const setFormValues = React.useCallback((newFormValues: Partial>) => { - setFormState((previousState) => ({ - ...previousState, - values: newFormValues, - })); - }, []); - - const setFormErrors = React.useCallback((newFormErrors: Partial>) => { - setFormState((previousState) => ({ - ...previousState, - errors: newFormErrors, - })); - }, []); - - const handleFormFieldChange = React.useCallback( - (name: keyof D, value: string | number | boolean | File | null) => { - const validateField = async (values: Partial>) => { - if (validate) { - const { issues } = await validate(values); - setFormErrors({ - ...formErrors, - [name]: issues?.find((issue) => issue.path?.[0] === name)?.message, - }); - } - }; - - const newFormValues = { ...formValues, [name]: value }; - - setFormValues(newFormValues); - validateField(newFormValues); - }, - [formErrors, formValues, setFormErrors, setFormValues, validate], - ); - - const handleFormReset = React.useCallback(() => { - setFormValues(initialValues); - }, [initialValues, setFormValues]); - - const handleFormSubmit = React.useCallback(async () => { - if (validate) { - const { issues } = await validate(formValues); - if (issues && issues.length > 0) { - setFormErrors(Object.fromEntries(issues.map((issue) => [issue.path?.[0], issue.message]))); - throw new Error('Form validation failed'); - } - } - setFormErrors({}); - - try { - await createOne(formValues); - notifications.show(localeText.createSuccessMessage, { - severity: 'success', - autoHideDuration: 3000, - }); - - if (onSubmitSuccess) { - await onSubmitSuccess(formValues); - } - - if (resetOnSubmit) { - handleFormReset(); - } - } catch (createError) { - notifications.show(`${localeText.createErrorMessage} ${(createError as Error).message}`, { - severity: 'error', - autoHideDuration: 3000, - }); - throw createError; - } - }, [ - createOne, - formValues, - handleFormReset, - localeText.createErrorMessage, - localeText.createSuccessMessage, - notifications, - onSubmitSuccess, - resetOnSubmit, - setFormErrors, - validate, - ]); - - return ( - - ); -} - -Create.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource: PropTypes.object, - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache: PropTypes.shape({ - cache: PropTypes.object.isRequired, - clear: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - set: PropTypes.func.isRequired, - ttl: PropTypes.number.isRequired, - }), - /** - * Initial form values. - * @default {} - */ - initialValues: PropTypes.object, - /** - * Locale text for the component. - */ - localeText: PropTypes.object, - /** - * Callback fired when the form is successfully submitted. - */ - onSubmitSuccess: PropTypes.func, - /** - * Whether the form fields should reset after the form is submitted. - * @default false - */ - resetOnSubmit: PropTypes.bool, -} as any; - -export { Create }; diff --git a/packages/toolpad-core/src/Crud/Crud.test.tsx b/packages/toolpad-core/src/Crud/Crud.test.tsx deleted file mode 100644 index 74c0ff6a101..00000000000 --- a/packages/toolpad-core/src/Crud/Crud.test.tsx +++ /dev/null @@ -1,352 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { expect, describe, test, afterEach } from 'vitest'; -import { render, screen, waitFor, within } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import '@testing-library/jest-dom/vitest'; -import { AppProvider, Router } from '../AppProvider'; -import { Crud } from './Crud'; -import type { DataModel, DataSource } from './types'; - -type OrderStatus = 'Pending' | 'Sent'; - -interface Order extends DataModel { - id: number; - title: string; - description?: string; - status: OrderStatus; - itemCount: number; - fastDelivery: boolean; - createdAt: string; - deliveryTime?: string; -} - -const INITIAL_ORDERS: Order[] = [ - { - id: 1, - title: 'Order 1', - description: 'I am the first order', - status: 'Pending', - itemCount: 1, - fastDelivery: true, - createdAt: new Date().toISOString(), - }, - { - id: 2, - title: 'Order 2', - description: 'I am the second order', - status: 'Pending', - itemCount: 2, - fastDelivery: false, - createdAt: new Date().toISOString(), - }, - { - id: 3, - title: 'Order 3', - description: 'I am the third order', - status: 'Sent', - itemCount: 3, - fastDelivery: true, - createdAt: new Date().toISOString(), - }, -]; - -let ordersStore: Order[] = INITIAL_ORDERS; - -function resetOrdersStore() { - ordersStore = INITIAL_ORDERS; -} - -const ordersDataSource: DataSource = { - fields: [ - { field: 'id', headerName: 'ID' }, - { field: 'title', headerName: 'Title' }, - { field: 'description', headerName: 'Description' }, - { - field: 'status', - headerName: 'Status', - type: 'singleSelect', - valueOptions: ['Pending', 'Sent'], - }, - { field: 'itemCount', headerName: 'No. of items', type: 'number' }, - { field: 'fastDelivery', headerName: 'Fast delivery', type: 'boolean' }, - { - field: 'createdAt', - headerName: 'Created at', - type: 'date', - valueGetter: (value: string) => value && new Date(value), - }, - { - field: 'deliveryTime', - headerName: 'Delivery time', - type: 'dateTime', - valueGetter: (value: string) => value && new Date(value), - }, - ], - getMany: ({ paginationModel }) => { - // Apply pagination - const start = paginationModel.page * paginationModel.pageSize; - const end = start + paginationModel.pageSize; - const paginatedOrders = ordersStore.slice(start, end); - - return { - items: paginatedOrders, - itemCount: ordersStore.length, - }; - }, - getOne: (orderId) => { - const orderToShow = ordersStore.find((order) => order.id === Number(orderId)); - - if (!orderToShow) { - throw new Error('Order not found'); - } - return orderToShow; - }, - createOne: (data) => { - const newOrder = { id: ordersStore.length + 1, ...data } as Order; - - ordersStore = [...ordersStore, newOrder]; - - return newOrder; - }, - updateOne: (orderId, data) => { - let updatedOrder: Order | null = null; - - ordersStore = ordersStore.map((order) => { - if (order.id === Number(orderId)) { - updatedOrder = { ...order, ...data }; - return updatedOrder; - } - return order; - }); - - if (!updatedOrder) { - throw new Error('Order not found'); - } - - return updatedOrder; - }, - deleteOne: (orderId) => { - ordersStore = ordersStore.filter((order) => order.id !== Number(orderId)); - }, - validate: (formValues) => { - let issues: { message: string; path: [keyof Order] }[] = []; - - if (!formValues.title) { - issues = [...issues, { message: 'Title is required', path: ['title'] }]; - } - if (formValues.title && formValues.title.length < 3) { - issues = [ - ...issues, - { - message: 'Title must be at least 3 characters long', - path: ['title'], - }, - ]; - } - if (!formValues.description) { - issues = [...issues, { message: 'Description is required', path: ['description'] }]; - } - - return { issues }; - }, -}; - -function AppWithRouter({ initialPath = '/' }: { initialPath: string }) { - const [url, setUrl] = React.useState(() => new URL(initialPath, window.location.origin)); - - const router = React.useMemo(() => { - return { - pathname: url.pathname, - searchParams: url.searchParams, - navigate: (newUrl) => { - const nextUrl = new URL(newUrl, window.location.origin); - if (nextUrl.pathname !== url.pathname || nextUrl.search !== url.search) { - setUrl(nextUrl); - } - }, - }; - }, [url.pathname, url.search, url.searchParams]); - - return ( - - - - ); -} - -describe('Crud', () => { - afterEach(() => { - resetOrdersStore(); - }); - - test('renders list items correctly', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('Order 1')).toBeInTheDocument(); - }); - - const renderedRows = await screen.findAllByRole('row'); - - const dataRows = renderedRows.slice(1); - - expect(within(dataRows[0]).getByText('I am the first order')).toBeInTheDocument(); - expect(within(dataRows[0]).getByText('Pending')).toBeInTheDocument(); - expect(within(dataRows[0]).getByText('yes')).toBeInTheDocument(); - - expect(within(dataRows[1]).getByText('Order 2')).toBeInTheDocument(); - expect(within(dataRows[1]).getByText('I am the second order')).toBeInTheDocument(); - expect(within(dataRows[1]).getByText('Pending')).toBeInTheDocument(); - expect(within(dataRows[1]).getByText('no')).toBeInTheDocument(); - - expect(within(dataRows[2]).getByText('Order 3')).toBeInTheDocument(); - expect(within(dataRows[2]).getByText('I am the third order')).toBeInTheDocument(); - expect(within(dataRows[2]).getByText('Sent')).toBeInTheDocument(); - expect(within(dataRows[2]).getByText('yes')).toBeInTheDocument(); - }); - - test('shows item details correctly', async () => { - const { unmount } = render(); - - await waitFor(() => { - expect(screen.getByText('Order 1')).toBeInTheDocument(); - }); - - expect(screen.getByText('I am the first order')).toBeInTheDocument(); - expect(screen.getByText('Pending')).toBeInTheDocument(); - expect(screen.getByText('Yes')).toBeInTheDocument(); - - expect(screen.queryByText('Order 2')).not.toBeInTheDocument(); - - unmount(); - render(); - - await waitFor(() => { - expect(screen.getByText('Order 2')).toBeInTheDocument(); - }); - - expect(screen.getByText('I am the second order')).toBeInTheDocument(); - expect(screen.getByText('Pending')).toBeInTheDocument(); - expect(screen.getByText('No')).toBeInTheDocument(); - - expect(screen.queryByText('Order 1')).not.toBeInTheDocument(); - }); - - test('creates new items', async () => { - render(); - - await userEvent.type(screen.getByLabelText('Description'), 'I am a new order'); - - await userEvent.click(screen.getByLabelText('Status')); - await userEvent.click(await screen.findByRole('option', { name: 'Sent' })); - - await userEvent.click(screen.getByLabelText('Fast delivery')); - - await userEvent.click(screen.getByRole('button', { name: 'Create' })); - - await waitFor(() => { - expect(screen.getByText('Order 1')).toBeInTheDocument(); - }); - - const renderedRows = await screen.findAllByRole('row'); - - const dataRows = renderedRows.slice(1); - - expect(within(dataRows[3]).getByText('New Order')).toBeInTheDocument(); - expect(within(dataRows[3]).getByText('I am a new order')).toBeInTheDocument(); - expect(within(dataRows[3]).getByText('Sent')).toBeInTheDocument(); - expect(within(dataRows[3]).getByText('yes')).toBeInTheDocument(); - }); - - test('edits existing items', async () => { - render(); - - await waitFor(() => { - expect(screen.getByLabelText('Title')).toHaveValue('Order 1'); - }); - - expect(screen.getByLabelText('Description')).toHaveValue('I am the first order'); - expect(screen.getByLabelText('Status')).toHaveTextContent('Pending'); - expect(screen.getByLabelText('Fast delivery')).toBeChecked(); - - await userEvent.clear(screen.getByLabelText('Title')); - await userEvent.type(screen.getByLabelText('Title'), 'Edited Order'); - - await userEvent.clear(screen.getByLabelText('Description')); - await userEvent.type(screen.getByLabelText('Description'), 'I am an edited order'); - - await userEvent.click(screen.getByLabelText('Status')); - await userEvent.click(await screen.findByRole('option', { name: 'Sent' })); - - await userEvent.click(screen.getByLabelText('Fast delivery')); - - await userEvent.click(screen.getByRole('button', { name: 'Edit' })); - - await waitFor(() => { - expect(screen.getByText('Order 2')).toBeInTheDocument(); - }); - - expect(screen.queryByText('Order 1')).not.toBeInTheDocument(); - - const renderedRows = await screen.findAllByRole('row'); - - const dataRows = renderedRows.slice(1); - - expect(within(dataRows[0]).getByText('Edited Order')).toBeInTheDocument(); - expect(within(dataRows[0]).getByText('I am an edited order')).toBeInTheDocument(); - expect(within(dataRows[0]).getByText('Sent')).toBeInTheDocument(); - expect(within(dataRows[0]).getByText('no')).toBeInTheDocument(); - }); - - test('deletes items from list view', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('Order 1')).toBeInTheDocument(); - }); - - const renderedRows = await screen.findAllByRole('row'); - const dataRows = renderedRows.slice(1); - - expect(dataRows).toHaveLength(3); - - await userEvent.click(within(dataRows[0]).getByLabelText('Delete')); - await userEvent.click(screen.getByRole('button', { name: 'Delete' })); - - expect(screen.queryByText('Order 1')).not.toBeInTheDocument(); - - const updatedRenderedRows = await screen.findAllByRole('row'); - const updatedDataRows = updatedRenderedRows.slice(1); - - expect(updatedDataRows).toHaveLength(2); - }); - - test('deletes items from detail view', async () => { - render(); - - await waitFor(() => { - expect(screen.getByText('Order 1')).toBeInTheDocument(); - }); - - await userEvent.click(screen.getByRole('button', { name: 'Delete' })); - await userEvent.click(screen.getByRole('button', { name: 'Delete' })); - - expect(screen.queryByText('Order 1')).not.toBeInTheDocument(); - - const updatedRenderedRows = await screen.findAllByRole('row'); - const updatedDataRows = updatedRenderedRows.slice(1); - - expect(updatedDataRows).toHaveLength(2); - }); -}); diff --git a/packages/toolpad-core/src/Crud/Crud.tsx b/packages/toolpad-core/src/Crud/Crud.tsx deleted file mode 100644 index 64df4b09653..00000000000 --- a/packages/toolpad-core/src/Crud/Crud.tsx +++ /dev/null @@ -1,182 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { match } from 'path-to-regexp'; -import invariant from 'invariant'; -import { RouterContext } from '../shared/context'; -import { CrudProvider } from './CrudProvider'; -import { List } from './List'; -import { Show } from './Show'; -import { Create } from './Create'; -import { Edit } from './Edit'; -import { DataSourceCache } from './cache'; -import type { DataModel, DataModelId, DataSource, OmitId } from './types'; - -export interface CrudProps { - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource: DataSource; - /** - * Root path to CRUD pages. - */ - rootPath: string; - /** - * Initial number of rows to show per page. - * @default 100 - */ - initialPageSize?: number; - /** - * Default form values for a new item. - * @default {} - */ - defaultValues?: Partial>; - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache?: DataSourceCache | null; -} -/** - * - * Demos: - * - * - [CRUD](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [Crud API](https://mui.com/toolpad/core/api/crud) - */ -function Crud(props: CrudProps) { - const { dataSource, rootPath, initialPageSize, defaultValues, dataSourceCache } = props; - - const listPath = rootPath; - const showPath = `${rootPath}/:id`; - const createPath = `${rootPath}/new`; - const editPath = `${rootPath}/:id/edit`; - - const routerContext = React.useContext(RouterContext); - - const handleRowClick = React.useCallback( - (id: string | number) => { - routerContext?.navigate(`${rootPath}/${String(id)}`); - }, - [rootPath, routerContext], - ); - - const handleCreateClick = React.useCallback(() => { - routerContext?.navigate(createPath); - }, [createPath, routerContext]); - - const handleEditClick = React.useCallback( - (id: string | number) => { - routerContext?.navigate(`${rootPath}/${String(id)}/edit`); - }, - [rootPath, routerContext], - ); - - const handleCreate = React.useCallback(() => { - routerContext?.navigate(listPath); - }, [listPath, routerContext]); - - const handleEdit = React.useCallback(() => { - routerContext?.navigate(listPath); - }, [listPath, routerContext]); - - const handleDelete = React.useCallback(() => { - routerContext?.navigate(listPath); - }, [listPath, routerContext]); - - const renderedRoute = React.useMemo(() => { - const pathname = routerContext?.pathname ?? ''; - - if (match(listPath)(pathname)) { - return ( - - initialPageSize={initialPageSize} - onRowClick={handleRowClick} - onCreateClick={handleCreateClick} - onEditClick={handleEditClick} - /> - ); - } - if (match(createPath)(pathname)) { - return ( - - initialValues={defaultValues} - onSubmitSuccess={handleCreate} - resetOnSubmit={false} - /> - ); - } - const showMatch = match<{ id: DataModelId }>(showPath)(pathname); - if (showMatch) { - const resourceId = showMatch.params.id; - invariant(resourceId, 'No resource ID present in URL.'); - return id={resourceId} onEditClick={handleEditClick} onDelete={handleDelete} />; - } - const editMatch = match<{ id: DataModelId }>(editPath)(pathname); - if (editMatch) { - const resourceId = editMatch.params.id; - invariant(resourceId, 'No resource ID present in URL.'); - return id={resourceId} onSubmitSuccess={handleEdit} />; - } - return null; - }, [ - routerContext?.pathname, - listPath, - createPath, - showPath, - editPath, - initialPageSize, - handleRowClick, - handleCreateClick, - handleEditClick, - defaultValues, - handleCreate, - handleDelete, - handleEdit, - ]); - - return ( - dataSource={dataSource} dataSourceCache={dataSourceCache}> - {renderedRoute} - - ); -} - -Crud.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource: PropTypes.object.isRequired, - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache: PropTypes.shape({ - cache: PropTypes.object.isRequired, - clear: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - set: PropTypes.func.isRequired, - ttl: PropTypes.number.isRequired, - }), - /** - * Default form values for a new item. - * @default {} - */ - defaultValues: PropTypes.object, - /** - * Initial number of rows to show per page. - * @default 100 - */ - initialPageSize: PropTypes.number, - /** - * Root path to CRUD pages. - */ - rootPath: PropTypes.string.isRequired, -} as any; - -export { Crud }; diff --git a/packages/toolpad-core/src/Crud/CrudForm.tsx b/packages/toolpad-core/src/Crud/CrudForm.tsx deleted file mode 100644 index 05694b5b75a..00000000000 --- a/packages/toolpad-core/src/Crud/CrudForm.tsx +++ /dev/null @@ -1,388 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import invariant from 'invariant'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import Checkbox from '@mui/material/Checkbox'; -import FormControl from '@mui/material/FormControl'; -import FormControlLabel from '@mui/material/FormControlLabel'; -import FormGroup from '@mui/material/FormGroup'; -import FormHelperText from '@mui/material/FormHelperText'; -import Grid from '@mui/material/Grid2'; -import InputLabel from '@mui/material/InputLabel'; -import MenuItem from '@mui/material/MenuItem'; -import Select, { SelectChangeEvent } from '@mui/material/Select'; -import TextField, { TextFieldProps } from '@mui/material/TextField'; -import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; -import { DatePicker } from '@mui/x-date-pickers/DatePicker'; -import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; -import { LocalizationProvider } from '@mui/x-date-pickers/LocalizationProvider'; -import type { GridSingleSelectColDef } from '@mui/x-data-grid'; -import dayjs, { Dayjs } from 'dayjs'; -import { CrudContext } from '../shared/context'; -import type { DataField, DataModel, DataSource, OmitId } from './types'; - -interface CrudFormState { - values: Partial>; - errors: Partial>; -} - -export interface CrudFormSlotProps { - textField?: TextFieldProps; -} - -export interface CrudFormSlots { - /** - * The text field component used in the form. - * @default TextField - */ - textField?: React.JSXElementConstructor; -} - -export interface CrudFormProps { - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource?: DataSource; - /** - * Form state object, including field values and errors. - */ - formState: CrudFormState; - /** - * Callback fired when a form field is changed. - */ - onFieldChange: ( - name: keyof D, - value: string | number | boolean | File | null, - ) => void | Promise; - /** - * Callback fired when the form is submitted. - */ - onSubmit: (formValues: Partial>) => void | Promise; - /** - * Callback fired when the form is reset. - */ - onReset?: (formValues: Partial>) => void | Promise; - /** - * The components used for each slot inside. - * @default {} - */ - slots?: CrudFormSlots; - /** - * The props used for each slot inside. - * @default {} - */ - slotProps?: CrudFormSlotProps; - /** - * Text for form submit button. - */ - submitButtonLabel: string; -} - -/** - * - * Demos: - * - * - [CRUD](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [CrudForm API](https://mui.com/toolpad/core/api/crud-form) - */ -function CrudForm(props: CrudFormProps) { - const { formState, onFieldChange, onSubmit, onReset, submitButtonLabel, slots, slotProps } = - props; - - const formValues = formState.values; - const formErrors = formState.errors; - - const crudContext = React.useContext(CrudContext); - const dataSource = (props.dataSource ?? crudContext.dataSource) as NonNullable< - typeof props.dataSource - >; - - invariant(dataSource, 'No data source found.'); - - const { fields } = dataSource; - - const [, submitAction, isSubmitting] = React.useActionState(async () => { - try { - await onSubmit(formValues); - } catch (error) { - return error as Error; - } - return null; - }, null); - - const handleTextFieldChange = React.useCallback( - (event: React.ChangeEvent) => { - onFieldChange(event.target.name, event.target.value); - }, - [onFieldChange], - ); - - const handleNumberFieldChange = React.useCallback( - (event: React.ChangeEvent) => { - onFieldChange(event.target.name, Number(event.target.value)); - }, - [onFieldChange], - ); - - const handleCheckboxFieldChange = React.useCallback( - (event: React.ChangeEvent, checked: boolean) => { - onFieldChange(event.target.name, checked); - }, - [onFieldChange], - ); - - const handleDateFieldChange = React.useCallback( - (name: string) => (value: Dayjs | null) => { - if (value?.isValid()) { - onFieldChange(name, value.toISOString() ?? null); - } else if (formValues[name]) { - onFieldChange(name, null); - } - }, - [formValues, onFieldChange], - ); - - const handleSelectFieldChange = React.useCallback( - (event: SelectChangeEvent) => { - onFieldChange(event.target.name, event.target.value); - }, - [onFieldChange], - ); - - const renderField = React.useCallback( - (formField: DataField) => { - const { field, type, headerName } = formField; - - const fieldValue = formValues[field]; - const fieldError = formErrors[field]; - - let fieldElement: React.ReactNode = null; - - if (!type || type === 'string') { - const TextFieldComponent = slots?.textField ?? TextField; - - fieldElement = ( - - ); - } - if (type === 'number') { - fieldElement = ( - - ); - } - if (type === 'boolean') { - fieldElement = ( - - - } - label={headerName} - /> - {fieldError ?? ' '} - - ); - } - if (type === 'date') { - fieldElement = ( - - - - ); - } - if (type === 'dateTime') { - fieldElement = ( - - - - ); - } - if (type === 'singleSelect') { - const { getOptionValue, getOptionLabel, valueOptions } = - formField as GridSingleSelectColDef; - - if (valueOptions && Array.isArray(valueOptions)) { - const labelId = `${field}-label`; - - fieldElement = ( - - {headerName} - - {fieldError ?? ' '} - - ); - } - } - - return ( - - {fieldElement} - - ); - }, - [ - formErrors, - formValues, - handleCheckboxFieldChange, - handleDateFieldChange, - handleNumberFieldChange, - handleSelectFieldChange, - handleTextFieldChange, - slotProps?.textField, - slots, - ], - ); - - const handleReset = React.useCallback(async () => { - if (onReset) { - await onReset(formValues); - } - }, [formValues, onReset]); - - return ( - - - - {fields.filter(({ field }) => field !== 'id').map(renderField)} - - - - - - - ); -} - -CrudForm.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource: PropTypes.object, - /** - * Form state object, including field values and errors. - */ - formState: PropTypes.shape({ - errors: PropTypes.object.isRequired, - values: PropTypes.object.isRequired, - }).isRequired, - /** - * Callback fired when a form field is changed. - */ - onFieldChange: PropTypes.func.isRequired, - /** - * Callback fired when the form is reset. - */ - onReset: PropTypes.func, - /** - * Callback fired when the form is submitted. - */ - onSubmit: PropTypes.func.isRequired, - /** - * The props used for each slot inside. - * @default {} - */ - slotProps: PropTypes.shape({ - textField: PropTypes.object, - }), - /** - * The components used for each slot inside. - * @default {} - */ - slots: PropTypes.shape({ - textField: PropTypes.elementType, - }), - /** - * Text for form submit button. - */ - submitButtonLabel: PropTypes.string.isRequired, -} as any; - -export { CrudForm }; diff --git a/packages/toolpad-core/src/Crud/CrudProvider.tsx b/packages/toolpad-core/src/Crud/CrudProvider.tsx deleted file mode 100644 index b302ab92791..00000000000 --- a/packages/toolpad-core/src/Crud/CrudProvider.tsx +++ /dev/null @@ -1,74 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { CrudContext } from '../shared/context'; -import { DataSourceCache } from './cache'; -import type { DataModel, DataSource } from './types'; - -export interface CrudProviderProps { - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource: DataSource; - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache?: DataSourceCache | null; - children?: React.ReactNode; -} -/** - * - * Demos: - * - * - [CRUD](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [CrudProvider API](https://mui.com/toolpad/core/api/crud-provider) - */ -function CrudProvider(props: CrudProviderProps) { - const { dataSource, dataSourceCache, children } = props; - - const cache = React.useMemo( - () => (typeof dataSourceCache !== 'undefined' ? dataSourceCache : new DataSourceCache()), - [dataSourceCache], - ); - - return ( - ['dataSource'], - dataSourceCache: cache, - }} - > - {children} - - ); -} - -CrudProvider.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * @ignore - */ - children: PropTypes.node, - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource: PropTypes.object.isRequired, - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache: PropTypes.shape({ - cache: PropTypes.object.isRequired, - clear: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - set: PropTypes.func.isRequired, - ttl: PropTypes.number.isRequired, - }), -} as any; - -export { CrudProvider }; diff --git a/packages/toolpad-core/src/Crud/Edit.tsx b/packages/toolpad-core/src/Crud/Edit.tsx deleted file mode 100644 index a842232b314..00000000000 --- a/packages/toolpad-core/src/Crud/Edit.tsx +++ /dev/null @@ -1,316 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Alert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; -import CircularProgress from '@mui/material/CircularProgress'; -import invariant from 'invariant'; -import { useNotifications } from '../useNotifications'; -import { CrudContext } from '../shared/context'; -import { useLocaleText } from '../AppProvider/LocalizationProvider'; -import { CrudForm } from './CrudForm'; -import { DataSourceCache } from './cache'; -import { useCachedDataSource } from './useCachedDataSource'; -import { CRUD_DEFAULT_LOCALE_TEXT, type CRUDLocaleText } from './localeText'; -import type { DataModel, DataModelId, DataSource, OmitId } from './types'; - -interface EditFormProps { - dataSource: DataSource & Required, 'getOne' | 'updateOne'>>; - initialValues: Partial>; - onSubmit: (formValues: Partial>) => void | Promise; - onSubmitSuccess?: (formValues: Partial>) => void | Promise; - localeText: CRUDLocaleText; -} - -function EditForm(props: EditFormProps) { - const { dataSource, initialValues, onSubmit, onSubmitSuccess, localeText } = props; - const { fields, validate } = dataSource; - - const notifications = useNotifications(); - - const [formState, setFormState] = React.useState<{ - values: Partial>; - errors: Partial>; - }>({ - values: { - ...Object.fromEntries( - fields - .filter(({ field }) => field !== 'id') - .map(({ field, type }) => [ - field, - type === 'boolean' ? (initialValues[field] ?? false) : initialValues[field], - ]), - ), - ...initialValues, - }, - errors: {}, - }); - const formValues = formState.values; - const formErrors = formState.errors; - - const setFormValues = React.useCallback((newFormValues: Partial>) => { - setFormState((previousState) => ({ - ...previousState, - values: newFormValues, - })); - }, []); - - const setFormErrors = React.useCallback((newFormErrors: Partial>) => { - setFormState((previousState) => ({ - ...previousState, - errors: newFormErrors, - })); - }, []); - - const handleFormFieldChange = React.useCallback( - (name: keyof D, value: string | number | boolean | File | null) => { - const validateField = async (values: Partial>) => { - if (validate) { - const { issues } = await validate(values); - setFormErrors({ - ...formErrors, - [name]: issues?.find((issue) => issue.path?.[0] === name)?.message, - }); - } - }; - - const newFormValues = { ...formValues, [name]: value }; - - setFormValues(newFormValues); - validateField(newFormValues); - }, - [formErrors, formValues, setFormErrors, setFormValues, validate], - ); - - const handleFormReset = React.useCallback(() => { - setFormValues(initialValues); - }, [initialValues, setFormValues]); - - const handleFormSubmit = React.useCallback(async () => { - if (validate) { - const { issues } = await validate(formValues); - if (issues && issues.length > 0) { - setFormErrors(Object.fromEntries(issues.map((issue) => [issue.path?.[0], issue.message]))); - throw new Error('Form validation failed'); - } - } - setFormErrors({}); - - try { - await onSubmit(formValues); - notifications.show(localeText.editSuccessMessage, { - severity: 'success', - autoHideDuration: 3000, - }); - - if (onSubmitSuccess) { - await onSubmitSuccess(formValues); - } - } catch (editError) { - notifications.show(`${localeText.editErrorMessage} ${(editError as Error).message}`, { - severity: 'error', - autoHideDuration: 3000, - }); - throw editError; - } - }, [ - formValues, - localeText.editErrorMessage, - localeText.editSuccessMessage, - notifications, - onSubmit, - onSubmitSuccess, - setFormErrors, - validate, - ]); - - return ( - - ); -} - -EditForm.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - dataSource: PropTypes.object.isRequired, - initialValues: PropTypes.object.isRequired, - localeText: PropTypes.object.isRequired, - onSubmit: PropTypes.func.isRequired, - onSubmitSuccess: PropTypes.func, -} as any; - -export interface EditProps { - id: DataModelId; - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource?: DataSource & Required, 'getOne' | 'updateOne'>>; - /** - * Callback fired when the form is successfully submitted. - */ - onSubmitSuccess?: (formValues: Partial>) => void | Promise; - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache?: DataSourceCache | null; - /** - * Locale text for the component. - */ - localeText?: CRUDLocaleText; -} - -/** - * - * Demos: - * - * - [CRUD](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [Edit API](https://mui.com/toolpad/core/api/edit) - */ -function Edit(props: EditProps) { - const { id, onSubmitSuccess, dataSourceCache, localeText: propsLocaleText } = props; - - const globalLocaleText = useLocaleText(); - - const crudContext = React.useContext(CrudContext); - const dataSource = (props.dataSource ?? crudContext.dataSource) as NonNullable< - typeof props.dataSource - >; - - invariant(dataSource, 'No data source found.'); - - const cache = React.useMemo(() => { - const manualCache = dataSourceCache ?? crudContext.dataSourceCache; - return typeof manualCache !== 'undefined' ? manualCache : new DataSourceCache(); - }, [crudContext.dataSourceCache, dataSourceCache]); - const cachedDataSource = useCachedDataSource(dataSource, cache) as NonNullable< - typeof props.dataSource - >; - - const { fields, validate, ...methods } = cachedDataSource; - const { getOne, updateOne } = methods; - - const [data, setData] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(false); - const [error, setError] = React.useState(null); - - const loadData = React.useCallback(async () => { - setError(null); - setIsLoading(true); - try { - const showData = await getOne(id); - setData(showData); - } catch (showDataError) { - setError(showDataError as Error); - } - setIsLoading(false); - }, [getOne, id]); - - React.useEffect(() => { - loadData(); - }, [loadData]); - - const handleSubmit = React.useCallback( - async (formValues: Partial>) => { - const updatedData = await updateOne(id, formValues); - setData(updatedData); - }, - [id, updateOne], - ); - - const renderEdit = React.useMemo(() => { - if (isLoading) { - return ( - - - - ); - } - if (error) { - return ( - - {error.message} - - ); - } - - const localeText = { ...CRUD_DEFAULT_LOCALE_TEXT, ...globalLocaleText, ...propsLocaleText }; - - return data ? ( - - ) : null; - }, [ - data, - dataSource, - error, - globalLocaleText, - handleSubmit, - isLoading, - onSubmitSuccess, - propsLocaleText, - ]); - - return {renderEdit}; -} - -Edit.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource: PropTypes.object, - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache: PropTypes.shape({ - cache: PropTypes.object.isRequired, - clear: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - set: PropTypes.func.isRequired, - ttl: PropTypes.number.isRequired, - }), - /** - * @ignore - */ - id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - /** - * Locale text for the component. - */ - localeText: PropTypes.object, - /** - * Callback fired when the form is successfully submitted. - */ - onSubmitSuccess: PropTypes.func, -} as any; - -export { Edit }; diff --git a/packages/toolpad-core/src/Crud/List.tsx b/packages/toolpad-core/src/Crud/List.tsx deleted file mode 100644 index 2348335f458..00000000000 --- a/packages/toolpad-core/src/Crud/List.tsx +++ /dev/null @@ -1,512 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { styled } from '@mui/material'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import IconButton from '@mui/material/IconButton'; -import Stack from '@mui/material/Stack'; -import Tooltip from '@mui/material/Tooltip'; -import Typography from '@mui/material/Typography'; -import { - DataGrid, - GridToolbar, - GridActionsCellItem, - DataGridProps, - GridColDef, - GridFilterModel, - GridPaginationModel, - GridSortModel, - GridEventListener, - gridClasses, -} from '@mui/x-data-grid'; -import type { DataGridProProps } from '@mui/x-data-grid-pro'; -import type { DataGridPremiumProps } from '@mui/x-data-grid-premium'; -import AddIcon from '@mui/icons-material/Add'; -import RefreshIcon from '@mui/icons-material/Refresh'; -import EditIcon from '@mui/icons-material/Edit'; -import DeleteIcon from '@mui/icons-material/Delete'; -import invariant from 'invariant'; -import { useDialogs } from '../useDialogs'; -import { useNotifications } from '../useNotifications'; -import { CrudContext, RouterContext, WindowContext } from '../shared/context'; -import { useLocaleText } from '../AppProvider/LocalizationProvider'; -import { DataSourceCache } from './cache'; -import { useCachedDataSource } from './useCachedDataSource'; -import type { DataModel, DataModelId, DataSource } from './types'; -import { CRUD_DEFAULT_LOCALE_TEXT, type CRUDLocaleText } from './localeText'; - -const ErrorOverlay = styled('div')(({ theme }) => ({ - position: 'absolute', - backgroundColor: theme.palette.error.light, - borderRadius: '4px', - top: 0, - height: '100%', - width: '100%', - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - textAlign: 'center', - p: 1, - zIndex: 10, -})); - -export interface ListSlotProps { - dataGrid?: Partial; -} - -export interface ListSlots { - /** - * The DataGrid component used to list the items. - * @default DataGrid - */ - dataGrid?: - | React.JSXElementConstructor - | React.JSXElementConstructor - | React.JSXElementConstructor; -} - -export interface ListProps { - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource?: DataSource & Required, 'getMany'>>; - /** - * Initial number of rows to show per page. - * @default 100 - */ - initialPageSize?: number; - /** - * Callback fired when a row is clicked. Not called if the target clicked is an interactive element added by the built-in columns. - */ - onRowClick?: (id: DataModelId) => void; - /** - * Callback fired when the "Create" button is clicked. - */ - onCreateClick?: () => void; - /** - * Callback fired when the "Edit" button is clicked. - */ - onEditClick?: (id: DataModelId) => void; - /** - * Callback fired when the item is successfully deleted. - */ - onDelete?: (id: DataModelId) => void; - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache?: DataSourceCache | null; - /** - * The components used for each slot inside. - * @default {} - */ - slots?: ListSlots; - /** - * The props used for each slot inside. - * @default {} - */ - slotProps?: ListSlotProps; - /** - * Locale text for the component. - */ - localeText?: CRUDLocaleText; -} - -/** - * - * Demos: - * - * - [CRUD](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [List API](https://mui.com/toolpad/core/api/list) - */ -function List(props: ListProps) { - const { - initialPageSize = 100, - onRowClick, - onCreateClick, - onEditClick, - onDelete, - dataSourceCache, - slots, - slotProps, - localeText: propsLocaleText, - } = props; - - const globalLocaleText = useLocaleText(); - const localeText = { ...CRUD_DEFAULT_LOCALE_TEXT, ...globalLocaleText, ...propsLocaleText }; - - const crudContext = React.useContext(CrudContext); - const dataSource = (props.dataSource ?? crudContext.dataSource) as NonNullable< - typeof props.dataSource - >; - - invariant(dataSource, 'No data source found.'); - - const cache = React.useMemo(() => { - const manualCache = dataSourceCache ?? crudContext.dataSourceCache; - return typeof manualCache !== 'undefined' ? manualCache : new DataSourceCache(); - }, [crudContext.dataSourceCache, dataSourceCache]); - const cachedDataSource = useCachedDataSource(dataSource, cache) as NonNullable< - typeof props.dataSource - >; - - const { fields, validate, ...methods } = cachedDataSource; - const { getMany, deleteOne } = methods; - - const routerContext = React.useContext(RouterContext); - const appWindowContext = React.useContext(WindowContext); - - const appWindow = appWindowContext ?? (typeof window !== 'undefined' ? window : null); - - const dialogs = useDialogs(); - const notifications = useNotifications(); - - const [rowsState, setRowsState] = React.useState<{ rows: D[]; rowCount: number }>({ - rows: [], - rowCount: 0, - }); - - const [paginationModel, setPaginationModel] = React.useState({ - page: routerContext?.searchParams.get('page') - ? Number(routerContext?.searchParams.get('page')) - : 0, - pageSize: routerContext?.searchParams.get('pageSize') - ? Number(routerContext?.searchParams.get('pageSize')) - : initialPageSize, - }); - const [filterModel, setFilterModel] = React.useState( - routerContext?.searchParams.get('filter') - ? JSON.parse(routerContext?.searchParams.get('filter') ?? '') - : { items: [] }, - ); - const [sortModel, setSortModel] = React.useState( - routerContext?.searchParams.get('sort') - ? JSON.parse(routerContext?.searchParams.get('sort') ?? '') - : [], - ); - - const [isLoading, setIsLoading] = React.useState(true); - const [error, setError] = React.useState(null); - - React.useEffect(() => { - if (appWindow) { - const url = new URL(appWindow.location.href); - - url.searchParams.set('page', String(paginationModel.page)); - url.searchParams.set('pageSize', String(paginationModel.pageSize)); - - if (!appWindow.frameElement) { - appWindow.history.pushState({}, '', url); - } - } - }, [appWindow, paginationModel.page, paginationModel.pageSize]); - - React.useEffect(() => { - if (appWindow) { - const url = new URL(appWindow.location.href); - - if ( - filterModel.items.length > 0 || - (filterModel.quickFilterValues && filterModel.quickFilterValues.length > 0) - ) { - url.searchParams.set('filter', JSON.stringify(filterModel)); - } else { - url.searchParams.delete('filter'); - } - - if (!appWindow.frameElement) { - appWindow.history.pushState({}, '', url); - } - } - }, [appWindow, filterModel]); - - React.useEffect(() => { - if (appWindow) { - const url = new URL(appWindow.location.href); - - if (sortModel.length > 0) { - url.searchParams.set('sort', JSON.stringify(sortModel)); - } else { - url.searchParams.delete('sort'); - } - - if (!appWindow.frameElement) { - appWindow.history.pushState({}, '', url); - } - } - }, [appWindow, sortModel]); - - const loadData = React.useCallback(async () => { - setError(null); - setIsLoading(true); - try { - const listData = await getMany({ - paginationModel, - sortModel, - filterModel, - }); - setRowsState({ - rows: listData.items, - rowCount: listData.itemCount, - }); - } catch (listDataError) { - setError(listDataError as Error); - } - setIsLoading(false); - }, [filterModel, getMany, paginationModel, sortModel]); - - React.useEffect(() => { - loadData(); - }, [filterModel, getMany, loadData, paginationModel, sortModel]); - - const handleRefresh = React.useCallback(() => { - if (!isLoading) { - cache?.clear(); - loadData(); - } - }, [cache, isLoading, loadData]); - - const handleRowClick = React.useCallback>( - ({ row }) => { - if (onRowClick) { - onRowClick(row.id); - } - }, - [onRowClick], - ); - - const handleItemEdit = React.useCallback( - (itemId: DataModelId) => () => { - if (onEditClick) { - onEditClick(itemId); - } - }, - [onEditClick], - ); - - const handleItemDelete = React.useCallback( - (itemId: DataModelId) => async () => { - const confirmed = await dialogs.confirm(localeText.deleteConfirmMessage, { - title: localeText.deleteConfirmTitle, - severity: 'error', - okText: localeText.deleteConfirmLabel, - cancelText: localeText.deleteCancelLabel, - }); - - if (confirmed) { - setIsLoading(true); - try { - await deleteOne?.(itemId); - - if (onDelete) { - onDelete(itemId); - } - - notifications.show(localeText.deleteSuccessMessage, { - severity: 'success', - autoHideDuration: 3000, - }); - loadData(); - } catch (deleteError) { - notifications.show(`${localeText.deleteErrorMessage} ${(deleteError as Error).message}`, { - severity: 'error', - autoHideDuration: 3000, - }); - } - setIsLoading(false); - } - }, - [ - deleteOne, - dialogs, - loadData, - localeText.deleteCancelLabel, - localeText.deleteConfirmLabel, - localeText.deleteConfirmMessage, - localeText.deleteConfirmTitle, - localeText.deleteErrorMessage, - localeText.deleteSuccessMessage, - notifications, - onDelete, - ], - ); - - const DataGridSlot = slots?.dataGrid ?? DataGrid; - - const initialState = React.useMemo( - () => ({ - pagination: { paginationModel: { pageSize: initialPageSize } }, - }), - [initialPageSize], - ); - - const columns = React.useMemo(() => { - return [ - ...fields, - { - field: 'actions', - type: 'actions', - flex: 1, - align: 'right', - getActions: ({ id }) => [ - ...(onEditClick - ? [ - } - label={localeText.editLabel} - onClick={handleItemEdit(id)} - />, - ] - : []), - ...(deleteOne - ? [ - } - label={localeText.deleteLabel} - onClick={handleItemDelete(id)} - />, - ] - : []), - ], - }, - ]; - }, [ - deleteOne, - fields, - handleItemDelete, - handleItemEdit, - localeText.deleteLabel, - localeText.editLabel, - onEditClick, - ]); - - return ( - - - -
- - - -
-
- {onCreateClick ? ( - - ) : null} -
- - )} - sx={{ - [`& .${gridClasses.columnHeader}, & .${gridClasses.cell}`]: { - outline: 'transparent', - }, - [`& .${gridClasses.columnHeader}:focus-within, & .${gridClasses.cell}:focus-within`]: { - outline: 'none', - }, - ...(onRowClick - ? { - [`& .${gridClasses.row}:hover`]: { - cursor: 'pointer', - }, - } - : {}), - ...slotProps?.dataGrid?.sx, - }} - /> - {error && ( - - {error.message} - - )} - -
- ); -} - -List.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource: PropTypes.object, - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache: PropTypes.shape({ - cache: PropTypes.object.isRequired, - clear: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - set: PropTypes.func.isRequired, - ttl: PropTypes.number.isRequired, - }), - /** - * Initial number of rows to show per page. - * @default 100 - */ - initialPageSize: PropTypes.number, - /** - * Locale text for the component. - */ - localeText: PropTypes.object, - /** - * Callback fired when the "Create" button is clicked. - */ - onCreateClick: PropTypes.func, - /** - * Callback fired when the item is successfully deleted. - */ - onDelete: PropTypes.func, - /** - * Callback fired when the "Edit" button is clicked. - */ - onEditClick: PropTypes.func, - /** - * Callback fired when a row is clicked. Not called if the target clicked is an interactive element added by the built-in columns. - */ - onRowClick: PropTypes.func, - /** - * The props used for each slot inside. - * @default {} - */ - slotProps: PropTypes.shape({ - dataGrid: PropTypes.object, - }), - /** - * The components used for each slot inside. - * @default {} - */ - slots: PropTypes.shape({ - dataGrid: PropTypes.func, - }), -} as any; - -export { List }; diff --git a/packages/toolpad-core/src/Crud/Show.tsx b/packages/toolpad-core/src/Crud/Show.tsx deleted file mode 100644 index d4b345b7e45..00000000000 --- a/packages/toolpad-core/src/Crud/Show.tsx +++ /dev/null @@ -1,314 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Alert from '@mui/material/Alert'; -import Box from '@mui/material/Box'; -import Button from '@mui/material/Button'; -import CircularProgress from '@mui/material/CircularProgress'; -import Divider from '@mui/material/Divider'; -import Grid from '@mui/material/Grid2'; -import Paper from '@mui/material/Paper'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import EditIcon from '@mui/icons-material/Edit'; -import DeleteIcon from '@mui/icons-material/Delete'; -import invariant from 'invariant'; -import dayjs from 'dayjs'; -import { useDialogs } from '../useDialogs'; -import { useNotifications } from '../useNotifications'; -import { useLocaleText } from '../AppProvider/LocalizationProvider'; -import { CrudContext } from '../shared/context'; -import { DataSourceCache } from './cache'; -import { useCachedDataSource } from './useCachedDataSource'; -import type { DataField, DataModel, DataModelId, DataSource } from './types'; -import { CRUD_DEFAULT_LOCALE_TEXT, type CRUDLocaleText } from './localeText'; - -export interface ShowProps { - id: DataModelId; - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource?: DataSource & Required, 'getOne'>>; - /** - * Callback fired when the "Edit" button is clicked. - */ - onEditClick?: (id: DataModelId) => void; - /** - * Callback fired when the item is successfully deleted. - */ - onDelete?: (id: DataModelId) => void; - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache?: DataSourceCache | null; - /** - * Locale text for the component. - */ - localeText?: CRUDLocaleText; -} - -/** - * - * Demos: - * - * - [CRUD](https://mui.com/toolpad/core/react-crud/) - * - * API: - * - * - [Show API](https://mui.com/toolpad/core/api/show) - */ -function Show(props: ShowProps) { - const { id, onEditClick, onDelete, dataSourceCache, localeText: propsLocaleText } = props; - - const globalLocaleText = useLocaleText(); - const localeText = { ...CRUD_DEFAULT_LOCALE_TEXT, ...globalLocaleText, ...propsLocaleText }; - - const crudContext = React.useContext(CrudContext); - const dataSource = (props.dataSource ?? crudContext.dataSource) as NonNullable< - typeof props.dataSource - >; - - invariant(dataSource, 'No data source found.'); - - const cache = React.useMemo(() => { - const manualCache = dataSourceCache ?? crudContext.dataSourceCache; - return typeof manualCache !== 'undefined' ? manualCache : new DataSourceCache(); - }, [crudContext.dataSourceCache, dataSourceCache]); - const cachedDataSource = useCachedDataSource(dataSource, cache) as NonNullable< - typeof props.dataSource - >; - - const { fields, validate, ...methods } = cachedDataSource; - const { getOne, deleteOne } = methods; - - const dialogs = useDialogs(); - const notifications = useNotifications(); - - const [data, setData] = React.useState(null); - const [isLoading, setIsLoading] = React.useState(false); - const [error, setError] = React.useState(null); - - const [hasDeleted, setHasDeleted] = React.useState(false); - - const loadData = React.useCallback(async () => { - setError(null); - setIsLoading(true); - try { - const showData = await getOne(id); - setData(showData); - } catch (showDataError) { - setError(showDataError as Error); - } - setIsLoading(false); - }, [getOne, id]); - - React.useEffect(() => { - loadData(); - }, [loadData]); - - const handleItemEdit = React.useCallback(() => { - if (onEditClick) { - onEditClick(id); - } - }, [id, onEditClick]); - - const handleItemDelete = React.useCallback(async () => { - const confirmed = await dialogs.confirm(localeText.deleteConfirmMessage, { - title: localeText.deleteConfirmTitle, - severity: 'error', - okText: localeText.deleteConfirmLabel, - cancelText: localeText.deleteCancelLabel, - }); - - if (confirmed) { - setIsLoading(true); - try { - await deleteOne?.(id); - - if (onDelete) { - onDelete(id); - } - - notifications.show(localeText.deleteSuccessMessage, { - severity: 'success', - autoHideDuration: 3000, - }); - - setHasDeleted(true); - } catch (deleteError) { - notifications.show(`${localeText.deleteErrorMessage} ${(deleteError as Error).message}`, { - severity: 'error', - autoHideDuration: 3000, - }); - } - setIsLoading(false); - } - }, [ - deleteOne, - dialogs, - id, - localeText.deleteCancelLabel, - localeText.deleteConfirmLabel, - localeText.deleteConfirmMessage, - localeText.deleteConfirmTitle, - localeText.deleteErrorMessage, - localeText.deleteSuccessMessage, - notifications, - onDelete, - ]); - - const renderField = React.useCallback( - (showField: DataField) => { - if (!data) { - return '…'; - } - - const { field, type } = showField; - const fieldValue = data[field]; - - if (type === 'boolean') { - return fieldValue ? 'Yes' : 'No'; - } - if (type === 'date') { - return fieldValue ? dayjs(fieldValue as string).format('MMMM D, YYYY') : '-'; - } - if (type === 'dateTime') { - return fieldValue ? dayjs(fieldValue as string).format('MMMM D, YYYY h:mm A') : '-'; - } - - return fieldValue ? String(fieldValue) : '-'; - }, - [data], - ); - - const renderShow = React.useMemo(() => { - if (isLoading) { - return ( - - - - ); - } - if (error) { - return ( - - {error.message} - - ); - } - - if (hasDeleted) { - return ( - - {localeText.deletedItemMessage} - - ); - } - - return data ? ( - - - {fields - .filter(({ type }) => type !== 'actions' && type !== 'custom') - .map((showField) => { - const { field, headerName } = showField; - - return ( - - - {headerName} - - {renderField(showField)} - - - - ); - })} - - - - {onEditClick ? ( - - ) : null} - {deleteOne ? ( - - ) : null} - - - ) : null; - }, [ - data, - deleteOne, - error, - fields, - handleItemDelete, - handleItemEdit, - hasDeleted, - isLoading, - localeText.deleteLabel, - localeText.deletedItemMessage, - localeText.editLabel, - onEditClick, - renderField, - ]); - - return {renderShow}; -} - -Show.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Server-side [data source](https://mui.com/toolpad/core/react-crud/#data-sources). - */ - dataSource: PropTypes.object, - /** - * [Cache](https://mui.com/toolpad/core/react-crud/#data-caching) for the data source. - */ - dataSourceCache: PropTypes.shape({ - cache: PropTypes.object.isRequired, - clear: PropTypes.func.isRequired, - get: PropTypes.func.isRequired, - set: PropTypes.func.isRequired, - ttl: PropTypes.number.isRequired, - }), - /** - * @ignore - */ - id: PropTypes.oneOfType([PropTypes.number, PropTypes.string]).isRequired, - /** - * Locale text for the component. - */ - localeText: PropTypes.object, - /** - * Callback fired when the item is successfully deleted. - */ - onDelete: PropTypes.func, - /** - * Callback fired when the "Edit" button is clicked. - */ - onEditClick: PropTypes.func, -} as any; - -export { Show }; diff --git a/packages/toolpad-core/src/Crud/cache.ts b/packages/toolpad-core/src/Crud/cache.ts deleted file mode 100644 index 0815f4a65ce..00000000000 --- a/packages/toolpad-core/src/Crud/cache.ts +++ /dev/null @@ -1,42 +0,0 @@ -export type DataSourceCacheConfig = { - /** - * Time To Live for each cache entry in milliseconds. - * After this time the cache entry will become stale and the next query will result in cache miss. - * @default 300000 (5 minutes) - */ - ttl?: number; -}; - -export class DataSourceCache { - private cache: Record; - - private ttl: number; - - constructor(config?: DataSourceCacheConfig) { - this.cache = {}; - this.ttl = config?.ttl ?? 300000; - } - - set(key: string, value: unknown) { - const expiry = Date.now() + this.ttl; - this.cache[key] = { value, expiry }; - } - - get(key: string): unknown | undefined { - const entry = this.cache[key]; - if (!entry) { - return undefined; - } - - if (Date.now() > entry.expiry) { - delete this.cache[key]; - return undefined; - } - - return entry.value; - } - - clear() { - this.cache = {}; - } -} diff --git a/packages/toolpad-core/src/Crud/index.ts b/packages/toolpad-core/src/Crud/index.ts deleted file mode 100644 index 9338f107010..00000000000 --- a/packages/toolpad-core/src/Crud/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -export * from './cache'; - -export * from './CrudForm'; - -export * from './List'; -export * from './Show'; -export * from './Create'; -export * from './Edit'; - -export * from './CrudProvider'; - -export * from './Crud'; - -export type * from './types'; diff --git a/packages/toolpad-core/src/Crud/localeText.ts b/packages/toolpad-core/src/Crud/localeText.ts deleted file mode 100644 index 43b7dfd5b3e..00000000000 --- a/packages/toolpad-core/src/Crud/localeText.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { LocaleText } from '../AppProvider/LocalizationProvider'; - -export type CRUDLocaleText = Pick< - LocaleText, - | 'createNewButtonLabel' - | 'reloadButtonLabel' - | 'createLabel' - | 'createSuccessMessage' - | 'createErrorMessage' - | 'editLabel' - | 'editSuccessMessage' - | 'editErrorMessage' - | 'deleteLabel' - | 'deleteConfirmTitle' - | 'deleteConfirmMessage' - | 'deleteConfirmLabel' - | 'deleteCancelLabel' - | 'deleteSuccessMessage' - | 'deleteErrorMessage' - | 'deletedItemMessage' ->; - -export const CRUD_DEFAULT_LOCALE_TEXT: CRUDLocaleText = { - createNewButtonLabel: 'Create new', - reloadButtonLabel: 'Reload data', - createLabel: 'Create', - createSuccessMessage: 'Item created successfully.', - createErrorMessage: 'Failed to create item. Reason:', - editLabel: 'Edit', - editSuccessMessage: 'Item edited successfully.', - editErrorMessage: 'Failed to edit item. Reason:', - deleteLabel: 'Delete', - deleteConfirmTitle: 'Delete item?', - deleteConfirmMessage: 'Do you wish to delete this item?', - deleteConfirmLabel: 'Delete', - deleteCancelLabel: 'Cancel', - deleteSuccessMessage: 'Item deleted successfully.', - deleteErrorMessage: 'Failed to delete item. Reason:', - deletedItemMessage: 'This item has been deleted.', -}; diff --git a/packages/toolpad-core/src/Crud/types.ts b/packages/toolpad-core/src/Crud/types.ts deleted file mode 100644 index 73b7da09271..00000000000 --- a/packages/toolpad-core/src/Crud/types.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - GridColDef, - GridColType, - GridFilterModel, - GridPaginationModel, - GridSortModel, -} from '@mui/x-data-grid'; -import type { StandardSchemaV1 } from '@standard-schema/spec'; - -export type DataModelId = string | number; - -export interface DataModel { - id: DataModelId; - [key: PropertyKey]: unknown; -} - -type RemappedOmit = { [P in keyof T as P extends K ? never : P]: T[P] }; - -export type OmitId = RemappedOmit; - -export type DataField = RemappedOmit & { type?: GridColType }; - -export interface DataSource { - fields: DataField[]; - getMany?: (params: { - paginationModel: GridPaginationModel; - sortModel: GridSortModel; - filterModel: GridFilterModel; - }) => { items: D[]; itemCount: number } | Promise<{ items: D[]; itemCount: number }>; - getOne?: (id: DataModelId) => D | Promise; - createOne?: (data: Partial>) => D | Promise; - updateOne?: (id: DataModelId, data: Partial>) => D | Promise; - deleteOne?: (id: DataModelId) => void | Promise; - /** - * Function to validate form values. Follows the Standard Schema `validate` function format (https://standardschema.dev/). - */ - validate?: ( - value: Partial>, - ) => ReturnType>>['~standard']['validate']>; -} diff --git a/packages/toolpad-core/src/Crud/useCachedDataSource.ts b/packages/toolpad-core/src/Crud/useCachedDataSource.ts deleted file mode 100644 index a5187be50f9..00000000000 --- a/packages/toolpad-core/src/Crud/useCachedDataSource.ts +++ /dev/null @@ -1,69 +0,0 @@ -import * as React from 'react'; -import { DataSourceCache } from './cache'; -import type { DataModel, DataSource } from './types'; - -function useCachedDataSource( - dataSource: DataSource, - cache: DataSourceCache | null, -): DataSource { - return React.useMemo(() => { - if (!cache) { - return dataSource; - } - - const { getMany, getOne, createOne, updateOne, deleteOne, ...rest } = dataSource; - - return { - ...Object.fromEntries( - Object.entries({ getMany, getOne }) - .filter(([_key, method]) => !!method) - .map(([key, method]) => [ - key, - async (...args: unknown[]) => { - const cacheKey = JSON.stringify([key, ...args]); - - const cacheValue = cache.get(cacheKey); - - if (cacheValue) { - return cacheValue; - } - - const result = await ( - method as ( - ...args: unknown[] - ) => - | ReturnType['getMany']>> - | ReturnType['getOne']>> - )(...args); - - cache.set(cacheKey, result); - return result; - }, - ]), - ), - ...Object.fromEntries( - Object.entries({ createOne, updateOne, deleteOne }) - .filter(([_key, method]) => !!method) - .map(([key, method]) => [ - key, - async (...args: unknown[]) => { - const result = await ( - method as ( - ...args: unknown[] - ) => - | ReturnType['createOne']>> - | ReturnType['updateOne']>> - | ReturnType['deleteOne']>> - )(...args); - - cache.clear(); - return result; - }, - ]), - ), - ...rest, - }; - }, [cache, dataSource]); -} - -export { useCachedDataSource }; diff --git a/packages/toolpad-core/src/DashboardLayout/AppTitle.tsx b/packages/toolpad-core/src/DashboardLayout/AppTitle.tsx deleted file mode 100644 index 4c393e01b51..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/AppTitle.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import * as React from 'react'; -import Typography from '@mui/material/Typography'; -import Stack from '@mui/material/Stack'; -import { styled, useTheme } from '@mui/material'; -import { Link } from '../shared/Link'; -import { ToolpadLogo } from './ToolpadLogo'; -import { type Branding } from '../AppProvider'; -import { useApplicationTitle } from '../shared/branding'; - -const LogoContainer = styled('div')({ - position: 'relative', - height: 40, - '& img': { - maxHeight: 40, - }, -}); - -export interface AppTitleProps { - branding?: Branding; -} - -/** - * @ignore - internal component. - */ -export function AppTitle(props: AppTitleProps) { - const theme = useTheme(); - const defaultTitle = useApplicationTitle(); - const title = props?.branding?.title ?? defaultTitle; - return ( - - - {props?.branding?.logo ?? } - - {title} - - - - ); -} diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx deleted file mode 100644 index 8b89572c504..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.test.tsx +++ /dev/null @@ -1,430 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test, expect, vi } from 'vitest'; -import { render, within, screen } from '@testing-library/react'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; -import BarChartIcon from '@mui/icons-material/BarChart'; -import DescriptionIcon from '@mui/icons-material/Description'; -import LayersIcon from '@mui/icons-material/Layers'; -import userEvent from '@testing-library/user-event'; -import '@testing-library/jest-dom/vitest'; -import { AppProvider, Navigation } from '../AppProvider'; -import { DashboardLayout } from './DashboardLayout'; - -describe('DashboardLayout', () => { - test('renders content correctly', async () => { - render(Hello world); - - expect(screen.getByText('Hello world')).toBeTruthy(); - }); - - test('renders branding correctly in header', async () => { - const BRANDING = { - title: 'My Company', - logo: Placeholder Logo, - }; - - render( - - Hello world - , - ); - - const header = screen.getByRole('banner'); - - expect(within(header).getByText('My Company')).toBeTruthy(); - expect(within(header).getByAltText('Placeholder Logo')).toBeTruthy(); - }); - - test('can switch theme', async () => { - const user = userEvent.setup(); - - render( - - Hello world - , - ); - - const getBackgroundColorCSSVariable = () => - getComputedStyle(document.documentElement).getPropertyValue( - '--mui-palette-common-background', - ); - - const header = screen.getByRole('banner'); - - const themeSwitcherButton = within(header).getByLabelText('Switch to dark mode'); - - expect(getBackgroundColorCSSVariable()).toBe('#fff'); - - await user.click(themeSwitcherButton); - - expect(getBackgroundColorCSSVariable()).toBe('#000'); - - await user.click(themeSwitcherButton); - - expect(getBackgroundColorCSSVariable()).toBe('#fff'); - }); - - test('navigation works correctly', async () => { - const NAVIGATION: Navigation = [ - { - kind: 'header', - title: 'Main items', - }, - { - title: 'Dashboard', - segment: 'dashboard', - icon: , - }, - { - title: 'Orders', - segment: 'orders', - icon: , - }, - { - kind: 'divider', - }, - { - kind: 'header', - title: 'Analytics', - }, - { - segment: 'reports', - title: 'Reports', - icon: , - children: [ - { - segment: 'sales', - title: 'Sales', - icon: , - }, - { - segment: 'traffic', - title: 'Traffic', - icon: , - }, - ], - }, - { - segment: 'integrations', - title: 'Integrations', - icon: , - }, - ]; - - const user = userEvent.setup(); - - render( - - Hello world - , - ); - - const desktopNavigation = screen.getByRole('navigation', { name: 'Desktop' }); - - // List subheaders are present - - expect(within(desktopNavigation).getByText('Main items')).toBeTruthy(); - expect(within(desktopNavigation).getByText('Analytics')).toBeTruthy(); - - // List items and their links are present - - const dashboardLink = within(desktopNavigation).getByRole('link', { name: 'Dashboard' }); - const ordersLink = within(desktopNavigation).getByRole('link', { name: 'Orders' }); - - expect(dashboardLink.getAttribute('href')).toBe('/dashboard'); - expect(ordersLink.getAttribute('href')).toBe('/orders'); - - const reportsItem = within(desktopNavigation).getByText('Reports'); - - expect(reportsItem).toBeTruthy(); - expect(within(desktopNavigation).getByText('Integrations')).toBeTruthy(); - - // Nested list items show when parent item is clicked - - expect(within(desktopNavigation).queryByText('Sales')).toBeNull(); - expect(within(desktopNavigation).queryByText('Traffic')).toBeNull(); - - await user.click(reportsItem); - - expect(within(desktopNavigation).getByText('Sales')).toBeTruthy(); - expect(within(desktopNavigation).getByText('Traffic')).toBeTruthy(); - }); - - test('starts with parent items expanded if any of their children is the current page', () => { - const NAVIGATION: Navigation = [ - { - segment: 'reports', - title: 'Reports', - icon: , - children: [ - { - segment: 'sales', - title: 'Sales', - icon: , - }, - { - segment: 'traffic', - title: 'Traffic', - icon: , - }, - ], - }, - ]; - - const mockRouter = { - pathname: '/reports/sales', - searchParams: new URLSearchParams(), - navigate: vi.fn(), - }; - - render( - - Hello world - , - ); - - const desktopNavigation = screen.getByRole('navigation', { name: 'Desktop' }); - - expect(within(desktopNavigation).getByText('Sales')).toBeTruthy(); - expect(within(desktopNavigation).getByText('Traffic')).toBeTruthy(); - }); - - test('shows correct selected page item', () => { - const NAVIGATION: Navigation = [ - { - title: 'Dashboard', - segment: 'dashboard', - icon: , - }, - { - title: 'Orders', - segment: 'orders', - icon: , - }, - { - segment: 'dynamic/override', - title: 'Dynamic Override', - icon: , - }, - { - segment: 'dynamicMoreOnly', - title: 'Dynamic', - icon: , - pattern: 'dynamic/:dynamicId', - }, - { - segment: 'optionalMoreOnly', - title: 'Optional', - pattern: 'optional{/:optionalId}?', - }, - { - segment: 'oneOrMore', - title: 'One or more', - pattern: 'oneormore{/:oneormoreId}+', - }, - { - segment: 'zeroOrMore', - title: 'Zero or more', - pattern: 'zeroormore{/:zeroormoreId}*', - }, - ]; - - function AppWithPathname({ pathname }: { pathname: string }) { - const mockRouter = { - pathname, - searchParams: new URLSearchParams(), - navigate: vi.fn(), - }; - - return ( - - Hello world - - ); - } - - const { rerender } = render(); - - const desktopNavigation = screen.getByRole('navigation', { name: 'Desktop' }); - - expect(within(desktopNavigation).getByRole('link', { name: 'Dashboard' })).toHaveClass( - 'Mui-selected', - ); - - rerender(); - - expect(within(desktopNavigation).getByRole('link', { name: 'Dashboard' })).not.toHaveClass( - 'Mui-selected', - ); - expect(within(desktopNavigation).getByRole('link', { name: 'Orders' })).toHaveClass( - 'Mui-selected', - ); - - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Dynamic' })).not.toHaveClass( - 'Mui-selected', - ); - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Dynamic' })).toHaveClass( - 'Mui-selected', - ); - expect( - within(desktopNavigation).getByRole('link', { name: 'Dynamic Override' }), - ).not.toHaveClass('Mui-selected'); - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Dynamic' })).not.toHaveClass( - 'Mui-selected', - ); - - // Does not show multiple selected items if a dynamic segment is overridden by a more specific segment - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Dynamic Override' })).toHaveClass( - 'Mui-selected', - ); - expect(within(desktopNavigation).getByRole('link', { name: 'Dynamic' })).not.toHaveClass( - 'Mui-selected', - ); - - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Optional' })).toHaveClass( - 'Mui-selected', - ); - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Optional' })).toHaveClass( - 'Mui-selected', - ); - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Optional' })).not.toHaveClass( - 'Mui-selected', - ); - - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'One or more' })).not.toHaveClass( - 'Mui-selected', - ); - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'One or more' })).toHaveClass( - 'Mui-selected', - ); - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'One or more' })).toHaveClass( - 'Mui-selected', - ); - - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Zero or more' })).toHaveClass( - 'Mui-selected', - ); - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Zero or more' })).toHaveClass( - 'Mui-selected', - ); - rerender(); - expect(within(desktopNavigation).getByRole('link', { name: 'Zero or more' })).toHaveClass( - 'Mui-selected', - ); - }); - - test('renders navigation actions', async () => { - const NAVIGATION: Navigation = [ - { - title: 'Item 1', - segment: 'item1', - icon: , - action:
Action 1
, - }, - { - title: 'Item 2', - segment: 'item2', - icon: , - action:
Action 2
, - }, - ]; - - render( - - Hello world - , - ); - - const desktopNavigation = screen.getByRole('navigation', { name: 'Desktop' }); - - expect(within(desktopNavigation).getByText('Action 1')).toBeTruthy(); - expect(within(desktopNavigation).getByText('Action 2')).toBeTruthy(); - }); - - test('renders sidebar footer slot content', async () => { - function SidebarFooter() { - return
I am footer
; - } - - render( - - Hello world - , - ); - - const desktopNavigation = screen.getByRole('navigation', { name: 'Desktop' }); - - expect(within(desktopNavigation).getByText('I am footer')).toBeTruthy(); - }); - - test('renders without the navigation and toggle button', async () => { - const NAVIGATION: Navigation = [ - { - title: 'Dashboard', - segment: 'dashboard', - icon: , - }, - { - title: 'Orders', - segment: 'orders', - icon: , - }, - ]; - - render( - - Hello world - , - ); - - const desktopNavigation = screen.queryByRole('navigation', { name: 'Desktop' }); - const navigationToggle = screen.queryByLabelText('Collapse menu'); - - // Expect that navigation and menu button are not rendered - expect(desktopNavigation).toBeNull(); - expect(navigationToggle).toBeNull(); - - // Ensure that main content is still rendered - expect(screen.getByText('Hello world')).toBeTruthy(); - }); - - test('renders without default collapsed navigation on desktop', async () => { - const NAVIGATION: Navigation = [ - { - title: 'Dashboard', - segment: 'dashboard', - icon: , - }, - ]; - - render( - - Hello world - , - ); - - // Expect that menu button has expand action - expect(screen.getAllByLabelText('Expand menu')).toBeTruthy(); - expect(screen.queryByLabelText('Collapse menu')).toBeNull(); - - // Ensure that main content is still rendered - expect(screen.getByText('Hello world')).toBeTruthy(); - }); -}); diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx deleted file mode 100644 index 4a3c29f6fb1..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/DashboardLayout.tsx +++ /dev/null @@ -1,594 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { styled, useTheme, SxProps } from '@mui/material'; -import MuiAppBar from '@mui/material/AppBar'; -import Box from '@mui/material/Box'; -import Drawer from '@mui/material/Drawer'; -import IconButton from '@mui/material/IconButton'; -import Stack from '@mui/material/Stack'; -import Toolbar from '@mui/material/Toolbar'; -import Tooltip from '@mui/material/Tooltip'; -import useMediaQuery from '@mui/material/useMediaQuery'; -import type {} from '@mui/material/themeCssVarsAugmentation'; -import MenuIcon from '@mui/icons-material/Menu'; -import MenuOpenIcon from '@mui/icons-material/MenuOpen'; -import { BrandingContext, NavigationContext, WindowContext } from '../shared/context'; -import { Account, type AccountProps } from '../Account'; -import { DashboardSidebarSubNavigation } from './DashboardSidebarSubNavigation'; -import { ToolbarActions } from './ToolbarActions'; -import { AppTitle, AppTitleProps } from './AppTitle'; -import { getDrawerSxTransitionMixin, getDrawerWidthTransitionMixin } from './utils'; -import { MINI_DRAWER_WIDTH } from './shared'; -import type { Branding, Navigation } from '../AppProvider'; - -const AppBar = styled(MuiAppBar)(({ theme }) => ({ - borderWidth: 0, - borderBottomWidth: 1, - borderStyle: 'solid', - borderColor: (theme.vars ?? theme).palette.divider, - boxShadow: 'none', - zIndex: theme.zIndex.drawer + 1, -})); - -export interface SidebarFooterProps { - mini: boolean; -} - -export interface DashboardLayoutSlotProps { - appTitle?: AppTitleProps; - toolbarActions?: {}; - toolbarAccount?: AccountProps; - sidebarFooter?: SidebarFooterProps; -} - -export interface DashboardLayoutSlots { - /** - * The component used for the app title section in the layout header. - * @default Link - */ - appTitle?: React.ElementType; - /** - * The toolbar actions component used in the layout header. - * @default ToolbarActions - */ - toolbarActions?: React.JSXElementConstructor<{}>; - /** - * The toolbar account component used in the layout header. - * @default Account - */ - toolbarAccount?: React.JSXElementConstructor; - /** - * Optional footer component used in the layout sidebar. - * @default null - */ - sidebarFooter?: React.JSXElementConstructor; -} - -export interface DashboardLayoutProps { - /** - * The content of the dashboard. - */ - children: React.ReactNode; - /** - * Branding options for the dashboard. - * @default null - */ - branding?: Branding | null; - /** - * Navigation definition for the dashboard. [Find out more](https://mui.com/toolpad/core/react-dashboard-layout/#navigation). - * @default [] - */ - navigation?: Navigation; - /** - * Whether the sidebar should start collapsed in desktop size screens. - * @default false - */ - defaultSidebarCollapsed?: boolean; - /** - * Whether the sidebar should not be collapsible to a mini variant in desktop and tablet viewports. - * @default false - */ - disableCollapsibleSidebar?: boolean; - /** - * Whether the navigation bar and menu icon should be hidden. - * @default false - */ - hideNavigation?: boolean; - /** - * Width of the sidebar when expanded. - * @default 320 - */ - sidebarExpandedWidth?: number | string; - /** - * The components used for each slot inside. - * @default {} - */ - slots?: DashboardLayoutSlots; - /** - * The props used for each slot inside. - * @default {} - */ - slotProps?: DashboardLayoutSlotProps; - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; -} - -/** - * - * Demos: - * - * - [Dashboard Layout](https://mui.com/toolpad/core/react-dashboard-layout/) - * - * API: - * - * - [DashboardLayout API](https://mui.com/toolpad/core/api/dashboard-layout) - */ -function DashboardLayout(props: DashboardLayoutProps) { - const { - children, - branding: brandingProp, - navigation: navigationProp, - defaultSidebarCollapsed = false, - disableCollapsibleSidebar = false, - hideNavigation = false, - sidebarExpandedWidth = 320, - slots, - slotProps, - sx, - } = props; - - const theme = useTheme(); - - const brandingContext = React.useContext(BrandingContext); - const navigationContext = React.useContext(NavigationContext); - const appWindowContext = React.useContext(WindowContext); - - const branding = { ...brandingContext, ...brandingProp }; - const navigation = navigationProp ?? navigationContext; - - const [isDesktopNavigationExpanded, setIsDesktopNavigationExpanded] = - React.useState(!defaultSidebarCollapsed); - const [isMobileNavigationExpanded, setIsMobileNavigationExpanded] = React.useState(false); - - const isOverSmViewport = useMediaQuery( - theme.breakpoints.up('sm'), - appWindowContext && { - matchMedia: appWindowContext.matchMedia, - }, - ); - const isOverMdViewport = useMediaQuery( - theme.breakpoints.up('md'), - appWindowContext && { - matchMedia: appWindowContext.matchMedia, - }, - ); - - const isNavigationExpanded = isOverMdViewport - ? isDesktopNavigationExpanded - : isMobileNavigationExpanded; - - const setIsNavigationExpanded = React.useCallback( - (newExpanded: boolean) => { - if (isOverMdViewport) { - setIsDesktopNavigationExpanded(newExpanded); - } else { - setIsMobileNavigationExpanded(newExpanded); - } - }, - [isOverMdViewport], - ); - - const [isNavigationFullyExpanded, setIsNavigationFullyExpanded] = - React.useState(isNavigationExpanded); - const [isNavigationFullyCollapsed, setIsNavigationFullyCollapsed] = - React.useState(!isNavigationExpanded); - - React.useEffect(() => { - if (isNavigationExpanded) { - const drawerWidthTransitionTimeout = setTimeout(() => { - setIsNavigationFullyExpanded(true); - }, theme.transitions.duration.enteringScreen); - - return () => clearTimeout(drawerWidthTransitionTimeout); - } - - setIsNavigationFullyExpanded(false); - - return () => {}; - }, [isNavigationExpanded, theme]); - - React.useEffect(() => { - if (!isNavigationExpanded) { - const drawerWidthTransitionTimeout = setTimeout(() => { - setIsNavigationFullyCollapsed(true); - }, theme.transitions.duration.leavingScreen); - - return () => clearTimeout(drawerWidthTransitionTimeout); - } - - setIsNavigationFullyCollapsed(false); - - return () => {}; - }, [isNavigationExpanded, theme]); - - const handleSetNavigationExpanded = React.useCallback( - (newExpanded: boolean) => () => { - setIsNavigationExpanded(newExpanded); - }, - [setIsNavigationExpanded], - ); - - const toggleNavigationExpanded = React.useCallback(() => { - setIsNavigationExpanded(!isNavigationExpanded); - }, [isNavigationExpanded, setIsNavigationExpanded]); - - const handleNavigationLinkClick = React.useCallback(() => { - setIsMobileNavigationExpanded(false); - }, [setIsMobileNavigationExpanded]); - - const isDesktopMini = !disableCollapsibleSidebar && !isDesktopNavigationExpanded; - const isMobileMini = !disableCollapsibleSidebar && !isMobileNavigationExpanded; - - const getMenuIcon = React.useCallback( - (isExpanded: boolean) => { - const expandMenuActionText = 'Expand'; - const collapseMenuActionText = 'Collapse'; - - return ( - -
- - {isExpanded ? : } - -
-
- ); - }, - [toggleNavigationExpanded], - ); - - const hasDrawerTransitions = isOverSmViewport && (!disableCollapsibleSidebar || isOverMdViewport); - - const ToolbarActionsSlot = slots?.toolbarActions ?? ToolbarActions; - const ToolbarAccountSlot = slots?.toolbarAccount ?? Account; - const SidebarFooterSlot = slots?.sidebarFooter ?? null; - - const getDrawerContent = React.useCallback( - (isMini: boolean, viewport: 'phone' | 'tablet' | 'desktop') => ( - - - - - {SidebarFooterSlot ? ( - - ) : null} - - - ), - [ - SidebarFooterSlot, - handleNavigationLinkClick, - hasDrawerTransitions, - isNavigationFullyCollapsed, - isNavigationFullyExpanded, - navigation, - slotProps?.sidebarFooter, - ], - ); - - const getDrawerSharedSx = React.useCallback( - (isMini: boolean, isTemporary: boolean) => { - const drawerWidth = isMini ? MINI_DRAWER_WIDTH : sidebarExpandedWidth; - - return { - displayPrint: 'none', - width: drawerWidth, - flexShrink: 0, - ...getDrawerWidthTransitionMixin(isNavigationExpanded), - ...(isTemporary ? { position: 'absolute' } : {}), - [`& .MuiDrawer-paper`]: { - position: 'absolute', - width: drawerWidth, - boxSizing: 'border-box', - backgroundImage: 'none', - ...getDrawerWidthTransitionMixin(isNavigationExpanded), - }, - }; - }, - [isNavigationExpanded, sidebarExpandedWidth], - ); - - const layoutRef = React.useRef(null); - - return ( - - - - - - {!hideNavigation ? ( - - - {getMenuIcon(isMobileNavigationExpanded)} - - - {getMenuIcon(isDesktopNavigationExpanded)} - - - ) : null} - {slots?.appTitle ? ( - - ) : ( - /* Hierarchy of application of `branding` - * 1. Branding prop passed in the `slotProps.appTitle` - * 2. Branding prop passed to the `DashboardLayout` - * 3. Branding prop passed to the `AppProvider` - */ - - )} - - - - - - - - - - {!hideNavigation ? ( - - - {getDrawerContent(false, 'phone')} - - - {getDrawerContent(isMobileMini, 'tablet')} - - - {getDrawerContent(isDesktopMini, 'desktop')} - - - ) : null} - - - - - {children} - - - - ); -} - -DashboardLayout.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * Branding options for the dashboard. - * @default null - */ - branding: PropTypes.shape({ - homeUrl: PropTypes.string, - logo: PropTypes.node, - title: PropTypes.string, - }), - /** - * The content of the dashboard. - */ - children: PropTypes.node, - /** - * Whether the sidebar should start collapsed in desktop size screens. - * @default false - */ - defaultSidebarCollapsed: PropTypes.bool, - /** - * Whether the sidebar should not be collapsible to a mini variant in desktop and tablet viewports. - * @default false - */ - disableCollapsibleSidebar: PropTypes.bool, - /** - * Whether the navigation bar and menu icon should be hidden. - * @default false - */ - hideNavigation: PropTypes.bool, - /** - * Navigation definition for the dashboard. [Find out more](https://mui.com/toolpad/core/react-dashboard-layout/#navigation). - * @default [] - */ - navigation: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.shape({ - action: PropTypes.node, - children: PropTypes.arrayOf( - PropTypes.oneOfType([ - PropTypes.object, - PropTypes.shape({ - kind: PropTypes.oneOf(['header']).isRequired, - title: PropTypes.string.isRequired, - }), - PropTypes.shape({ - kind: PropTypes.oneOf(['divider']).isRequired, - }), - ]).isRequired, - ), - icon: PropTypes.node, - kind: PropTypes.oneOf(['page']), - pattern: PropTypes.string, - segment: PropTypes.string, - title: PropTypes.string, - }), - PropTypes.shape({ - kind: PropTypes.oneOf(['header']).isRequired, - title: PropTypes.string.isRequired, - }), - PropTypes.shape({ - kind: PropTypes.oneOf(['divider']).isRequired, - }), - ]).isRequired, - ), - /** - * Width of the sidebar when expanded. - * @default 320 - */ - sidebarExpandedWidth: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - /** - * The props used for each slot inside. - * @default {} - */ - slotProps: PropTypes.shape({ - appTitle: PropTypes.shape({ - branding: PropTypes.shape({ - homeUrl: PropTypes.string, - logo: PropTypes.node, - title: PropTypes.string, - }), - }), - sidebarFooter: PropTypes.shape({ - mini: PropTypes.bool.isRequired, - }), - toolbarAccount: PropTypes.shape({ - localeText: PropTypes.object, - slotProps: PropTypes.shape({ - popover: PropTypes.object, - popoverContent: PropTypes.object, - preview: PropTypes.object, - signInButton: PropTypes.object, - signOutButton: PropTypes.object, - }), - slots: PropTypes.shape({ - popover: PropTypes.elementType, - popoverContent: PropTypes.elementType, - preview: PropTypes.elementType, - signInButton: PropTypes.elementType, - signOutButton: PropTypes.elementType, - }), - }), - toolbarActions: PropTypes.object, - }), - /** - * The components used for each slot inside. - * @default {} - */ - slots: PropTypes.shape({ - appTitle: PropTypes.elementType, - sidebarFooter: PropTypes.elementType, - toolbarAccount: PropTypes.elementType, - toolbarActions: PropTypes.elementType, - }), - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), -} as any; - -export { DashboardLayout }; diff --git a/packages/toolpad-core/src/DashboardLayout/DashboardSidebarSubNavigation.tsx b/packages/toolpad-core/src/DashboardLayout/DashboardSidebarSubNavigation.tsx deleted file mode 100644 index c33fa6d6862..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/DashboardSidebarSubNavigation.tsx +++ /dev/null @@ -1,368 +0,0 @@ -'use client'; -import * as React from 'react'; -import { styled, type Theme, SxProps } from '@mui/material'; -import Avatar from '@mui/material/Avatar'; -import Box from '@mui/material/Box'; -import Collapse from '@mui/material/Collapse'; -import Divider from '@mui/material/Divider'; -import Grow from '@mui/material/Grow'; -import List from '@mui/material/List'; -import ListItem from '@mui/material/ListItem'; -import ListItemButton from '@mui/material/ListItemButton'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import ListItemText from '@mui/material/ListItemText'; -import ListSubheader from '@mui/material/ListSubheader'; -import Paper from '@mui/material/Paper'; -import Typography from '@mui/material/Typography'; -import type {} from '@mui/material/themeCssVarsAugmentation'; -import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; -import { Link } from '../shared/Link'; -import { NavigationContext } from '../shared/context'; -import type { Navigation } from '../AppProvider'; -import { - getItemPath, - getItemTitle, - hasSelectedNavigationChildren, - isPageItem, -} from '../shared/navigation'; -import { getDrawerSxTransitionMixin } from './utils'; -import { MINI_DRAWER_WIDTH } from './shared'; -import { useActivePage } from '../useActivePage'; - -const NavigationListItemButton = styled(ListItemButton)(({ theme }) => ({ - borderRadius: 8, - '&.Mui-selected': { - '& .MuiListItemIcon-root': { - color: (theme.vars ?? theme).palette.primary.dark, - }, - '& .MuiTypography-root': { - color: (theme.vars ?? theme).palette.primary.dark, - }, - '& .MuiSvgIcon-root': { - color: (theme.vars ?? theme).palette.primary.dark, - }, - '& .MuiAvatar-root': { - backgroundColor: (theme.vars ?? theme).palette.primary.dark, - }, - '& .MuiTouchRipple-child': { - backgroundColor: (theme.vars ?? theme).palette.primary.dark, - }, - }, - '& .MuiSvgIcon-root': { - color: (theme.vars ?? theme).palette.action.active, - }, - '& .MuiAvatar-root': { - backgroundColor: (theme.vars ?? theme).palette.action.active, - }, -})); - -interface DashboardSidebarSubNavigationProps { - subNavigation: Navigation; - depth?: number; - onLinkClick: () => void; - isMini?: boolean; - isPopover?: boolean; - isFullyExpanded?: boolean; - isFullyCollapsed?: boolean; - hasDrawerTransitions?: boolean; -} - -/** - * @ignore - internal component. - */ -function DashboardSidebarSubNavigation({ - subNavigation, - depth = 0, - onLinkClick, - isMini = false, - isPopover = false, - isFullyExpanded = true, - isFullyCollapsed = false, - hasDrawerTransitions = false, -}: DashboardSidebarSubNavigationProps) { - const navigationContext = React.useContext(NavigationContext); - - const activePage = useActivePage(); - - const initialExpandedSidebarItemIds = React.useMemo( - () => - subNavigation - .map((navigationItem, navigationItemIndex) => ({ - navigationItem, - originalIndex: navigationItemIndex, - })) - .filter( - ({ navigationItem }) => - isPageItem(navigationItem) && - !!activePage && - hasSelectedNavigationChildren(navigationContext, navigationItem, activePage.path), - ) - .map(({ originalIndex }) => `${depth}-${originalIndex}`), - [activePage, depth, navigationContext, subNavigation], - ); - - const [expandedSidebarItemIds, setExpandedSidebarItemIds] = React.useState( - initialExpandedSidebarItemIds, - ); - const [hoveredMiniSidebarItemId, setHoveredMiniSidebarItemId] = React.useState( - null, - ); - - const handleOpenFolderClick = React.useCallback( - (itemId: string) => () => { - setExpandedSidebarItemIds((previousValue) => - previousValue.includes(itemId) - ? previousValue.filter((previousValueItemId) => previousValueItemId !== itemId) - : [...previousValue, itemId], - ); - }, - [], - ); - - return ( - - {subNavigation.map((navigationItem, navigationItemIndex) => { - if (navigationItem.kind === 'header') { - return ( - - {getItemTitle(navigationItem)} - - ); - } - - if (navigationItem.kind === 'divider') { - const nextItem = subNavigation[navigationItemIndex + 1]; - - return ( - - ); - } - - const navigationItemFullPath = getItemPath(navigationContext, navigationItem); - const navigationItemId = `${depth}-${navigationItemIndex}`; - const navigationItemTitle = getItemTitle(navigationItem); - - const isNestedNavigationExpanded = expandedSidebarItemIds.includes(navigationItemId); - - const listItemIconSize = 34; - - const isActive = - !!activePage && activePage.path === getItemPath(navigationContext, navigationItem); - - let nestedNavigationCollapseSx: SxProps = { display: 'none' }; - if (isMini && isFullyCollapsed) { - nestedNavigationCollapseSx = { - fontSize: 18, - position: 'absolute', - top: '41.5%', - right: '2px', - transform: 'translateY(-50%) rotate(-90deg)', - }; - } else if (!isMini && isFullyExpanded) { - nestedNavigationCollapseSx = { - ml: 0.5, - transform: `rotate(${isNestedNavigationExpanded ? 0 : -90}deg)`, - transition: (theme: Theme) => - theme.transitions.create('transform', { - easing: theme.transitions.easing.sharp, - duration: 100, - }), - }; - } - - // Show as selected in mini sidebar if any of the children matches path, otherwise show as selected if item matches path - const isSelected = - activePage && navigationItem.children && isMini - ? hasSelectedNavigationChildren(navigationContext, navigationItem, activePage.path) - : isActive && !navigationItem.children; - - const listItem = ( - { - setHoveredMiniSidebarItemId(navigationItemId); - }, - onMouseLeave: () => { - setHoveredMiniSidebarItemId(null); - }, - } - : {})} - sx={{ - py: 0, - px: 1, - overflowX: 'hidden', - }} - > - - {navigationItem.icon || isMini ? ( - - - {navigationItem.icon ?? null} - {!navigationItem.icon && isMini ? ( - - {navigationItemTitle - .split(' ') - .slice(0, 2) - .map((itemTitleWord) => itemTitleWord.charAt(0).toUpperCase())} - - ) : null} - - {isMini ? ( - - {navigationItemTitle} - - ) : null} - - ) : null} - {!isMini ? ( - - ) : null} - {navigationItem.action && !isMini && isFullyExpanded ? navigationItem.action : null} - {navigationItem.children ? : null} - - {navigationItem.children && isMini ? ( - - - - - - - - ) : null} - - ); - - return ( - - {listItem} - {navigationItem.children && !isMini ? ( - - - - ) : null} - - ); - })} - - ); -} - -export { DashboardSidebarSubNavigation }; diff --git a/packages/toolpad-core/src/DashboardLayout/ThemeSwitcher.tsx b/packages/toolpad-core/src/DashboardLayout/ThemeSwitcher.tsx deleted file mode 100644 index 927b8695a61..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/ThemeSwitcher.tsx +++ /dev/null @@ -1,78 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useTheme } from '@mui/material'; -import IconButton from '@mui/material/IconButton'; -import Tooltip from '@mui/material/Tooltip'; -import DarkModeIcon from '@mui/icons-material/DarkMode'; -import LightModeIcon from '@mui/icons-material/LightMode'; -import useSsr from '@toolpad/utils/hooks/useSsr'; -import { PaletteModeContext } from '../shared/context'; - -/** - * - * Demos: - * - * - [Dashboard Layout](https://mui.com/toolpad/core/react-dashboard-layout/) - * - * API: - * - * - [ThemeSwitcher API](https://mui.com/toolpad/core/api/theme-switcher) - */ -function ThemeSwitcher() { - const isSsr = useSsr(); - const theme = useTheme(); - - const { paletteMode, setPaletteMode, isDualTheme } = React.useContext(PaletteModeContext); - - const toggleMode = React.useCallback(() => { - setPaletteMode(paletteMode === 'dark' ? 'light' : 'dark'); - }, [paletteMode, setPaletteMode]); - - return isDualTheme ? ( - -
- - {theme.getColorSchemeSelector ? ( - - - - - ) : ( - - {isSsr || paletteMode !== 'dark' ? : } - - )} - -
-
- ) : null; -} - -export { ThemeSwitcher }; diff --git a/packages/toolpad-core/src/DashboardLayout/ToolbarActions.tsx b/packages/toolpad-core/src/DashboardLayout/ToolbarActions.tsx deleted file mode 100644 index 19a2717870f..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/ToolbarActions.tsx +++ /dev/null @@ -1,24 +0,0 @@ -'use client'; -import * as React from 'react'; -import Stack from '@mui/material/Stack'; -import { ThemeSwitcher } from './ThemeSwitcher'; - -/** - * - * Demos: - * - * - [Dashboard Layout](https://mui.com/toolpad/core/react-dashboard-layout/) - * - * API: - * - * - [ToolbarActions API](https://mui.com/toolpad/core/api/toolbar-actions) - */ -function ToolbarActions() { - return ( - - - - ); -} - -export { ToolbarActions }; diff --git a/packages/toolpad-core/src/DashboardLayout/ToolpadLogo.tsx b/packages/toolpad-core/src/DashboardLayout/ToolpadLogo.tsx deleted file mode 100644 index 1a9eae2db7d..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/ToolpadLogo.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ -function ToolpadLogo({ size = 40 }: { size?: number }) { - return ( - - - - - - - - - - - - - - - - - - - - - - - - ); -} - -export { ToolpadLogo }; diff --git a/packages/toolpad-core/src/DashboardLayout/index.ts b/packages/toolpad-core/src/DashboardLayout/index.ts deleted file mode 100644 index 31b6ce8a976..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './DashboardLayout'; -export * from './ToolbarActions'; -export * from './ThemeSwitcher'; diff --git a/packages/toolpad-core/src/DashboardLayout/shared.ts b/packages/toolpad-core/src/DashboardLayout/shared.ts deleted file mode 100644 index 8fb17ea0d70..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/shared.ts +++ /dev/null @@ -1 +0,0 @@ -export const MINI_DRAWER_WIDTH = 84; // px diff --git a/packages/toolpad-core/src/DashboardLayout/utils.ts b/packages/toolpad-core/src/DashboardLayout/utils.ts deleted file mode 100644 index 05a7be99ebe..00000000000 --- a/packages/toolpad-core/src/DashboardLayout/utils.ts +++ /dev/null @@ -1,20 +0,0 @@ -import type { Theme } from '@mui/material'; - -export function getDrawerSxTransitionMixin(isExpanded: boolean, property: string) { - return { - transition: (theme: Theme) => - theme.transitions.create(property, { - easing: theme.transitions.easing.sharp, - duration: isExpanded - ? theme.transitions.duration.enteringScreen - : theme.transitions.duration.leavingScreen, - }), - }; -} - -export function getDrawerWidthTransitionMixin(isExpanded: boolean) { - return { - ...getDrawerSxTransitionMixin(isExpanded, 'width'), - overflowX: 'hidden', - }; -} diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.test.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.test.tsx deleted file mode 100644 index 19ee4085eeb..00000000000 --- a/packages/toolpad-core/src/PageContainer/PageContainer.test.tsx +++ /dev/null @@ -1,166 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { expect, describe, test, vi } from 'vitest'; -import describeConformance from '@toolpad/utils/describeConformance'; -import { render, within, screen } from '@testing-library/react'; -import { userEvent } from '@testing-library/user-event'; -import { PageContainer } from './PageContainer'; -import { AppProvider } from '../AppProvider'; - -describe('PageContainer', () => { - describeConformance(, () => ({ - skip: ['themeDefaultProps'], - slots: { - header: {}, - }, - })); - - test('renders page container correctly', async () => { - const user = await userEvent.setup(); - const router = { pathname: '/orders', searchParams: new URLSearchParams(), navigate: vi.fn() }; - render( - - - , - ); - - const breadcrumbs = screen.getByRole('navigation', { name: 'breadcrumb' }); - - const homeLink = within(breadcrumbs).getByRole('link', { name: 'Home' }); - await user.click(homeLink); - - expect(router.navigate).toHaveBeenCalledWith('/', expect.objectContaining({})); - router.navigate.mockClear(); - - expect(within(breadcrumbs).getByText('Orders')).toBeTruthy(); - - expect(screen.getByText('Orders', { ignore: 'nav *' })); - }); - - test('renders nested', async () => { - const navigation = [ - { segment: '', title: 'ACME' }, - { - segment: 'home', - title: 'Home', - children: [ - { - segment: 'orders', - title: 'Orders', - }, - ], - }, - ]; - - const router = { - pathname: '/home/orders', - searchParams: new URLSearchParams(), - navigate: vi.fn(), - }; - - const branding = { title: 'ACME' }; - render( - - - , - ); - - const breadcrumbs = screen.getByRole('navigation', { name: 'breadcrumb' }); - - expect(within(breadcrumbs).getByText('ACME')).toBeTruthy(); - expect(within(breadcrumbs).getByText('Home')).toBeTruthy(); - expect(within(breadcrumbs).getByText('Orders')).toBeTruthy(); - }); - - test('renders dynamic correctly', async () => { - const user = await userEvent.setup(); - const router = { - pathname: '/orders/123', - searchParams: new URLSearchParams(), - navigate: vi.fn(), - }; - render( - - - , - ); - - const breadcrumbs = screen.getByRole('navigation', { name: 'breadcrumb' }); - - const homeLink = within(breadcrumbs).getByRole('link', { name: 'Home' }); - await user.click(homeLink); - - expect(router.navigate).toHaveBeenCalledWith('/', expect.objectContaining({})); - router.navigate.mockClear(); - - expect(within(breadcrumbs).getByText('Orders')).toBeTruthy(); - - expect(screen.getByText('Orders', { ignore: 'nav *' })); - }); - - test('renders nested dynamic correctly', async () => { - const router = { - pathname: '/users/invoices/123', - searchParams: new URLSearchParams(), - navigate: vi.fn(), - }; - render( - - - , - ); - - const breadcrumbs = screen.getByRole('navigation', { name: 'breadcrumb' }); - - const homeLink = within(breadcrumbs).getByRole('link', { name: 'Users' }); - expect(homeLink.getAttribute('href')).toBe('/users'); - expect(within(breadcrumbs).getByText('Invoices')).toBeTruthy(); - }); - - test('renders custom breadcrumbs', async () => { - render( - , - ); - - const breadcrumbs = screen.getByRole('navigation', { name: 'breadcrumb' }); - - const helloLink = within(breadcrumbs).getByRole('link', { name: 'Hello' }); - expect(helloLink.getAttribute('href')).toBe('/hello'); - expect(within(breadcrumbs).getByText('World')).toBeTruthy(); - }); - - test("doesn't spread title to child", async () => { - render(); - - expect(screen.queryByTitle('Hello World')).not.toBeInTheDocument(); - }); -}); diff --git a/packages/toolpad-core/src/PageContainer/PageContainer.tsx b/packages/toolpad-core/src/PageContainer/PageContainer.tsx deleted file mode 100644 index 9e7efa226aa..00000000000 --- a/packages/toolpad-core/src/PageContainer/PageContainer.tsx +++ /dev/null @@ -1,140 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Box from '@mui/material/Box'; -import Container, { ContainerProps } from '@mui/material/Container'; -import Stack from '@mui/material/Stack'; -import { SxProps } from '@mui/material'; -import { PageHeader, PageHeaderProps } from './PageHeader'; - -export interface Breadcrumb { - /** - * The title of the breadcrumb segment. - */ - title: string; - /** - * The path the breadcrumb links to. - */ - path?: string; -} -export interface PageContainerSlotProps { - header: PageHeaderProps; -} - -export interface PageContainerSlots { - /** - * The component that renders the page header. - * @default PageHeader - */ - header: React.ElementType; -} - -export interface PageContainerProps extends ContainerProps { - children?: React.ReactNode; - /** - * The title of the page. Leave blank to use the active page title. - */ - title?: string; - /** - * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs. - */ - breadcrumbs?: Breadcrumb[]; - /** - * The components used for each slot inside. - */ - slots?: PageContainerSlots; - /** - * The props used for each slot inside. - */ - slotProps?: PageContainerSlotProps; - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx?: SxProps; -} - -/** - * A container component to provide a title and breadcrumbs for your pages. - * - * Demos: - * - * - [Page Container](https://mui.com/toolpad/core/react-page-container/) - * - * API: - * - * - [PageContainer API](https://mui.com/toolpad/core/api/page-container) - */ -function PageContainer(props: PageContainerProps) { - const { children, breadcrumbs, slots, slotProps, title, ...rest } = props; - - const PageHeaderSlot = slots?.header ?? PageHeader; - - return ( - - - - {children} - - - ); -} - -PageContainer.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs. - */ - breadcrumbs: PropTypes.arrayOf( - PropTypes.shape({ - path: PropTypes.string, - title: PropTypes.string.isRequired, - }), - ), - /** - * @ignore - */ - children: PropTypes.node, - /** - * The props used for each slot inside. - */ - slotProps: PropTypes.shape({ - header: PropTypes.shape({ - breadcrumbs: PropTypes.arrayOf( - PropTypes.shape({ - path: PropTypes.string, - title: PropTypes.string.isRequired, - }), - ), - slotProps: PropTypes.shape({ - toolbar: PropTypes.object.isRequired, - }), - slots: PropTypes.shape({ - toolbar: PropTypes.elementType, - }), - title: PropTypes.string, - }).isRequired, - }), - /** - * The components used for each slot inside. - */ - slots: PropTypes.shape({ - header: PropTypes.elementType, - }), - /** - * The system prop that allows defining system overrides as well as additional CSS styles. - */ - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), - /** - * The title of the page. Leave blank to use the active page title. - */ - title: PropTypes.string, -} as any; - -export { PageContainer }; diff --git a/packages/toolpad-core/src/PageContainer/PageHeader.tsx b/packages/toolpad-core/src/PageContainer/PageHeader.tsx deleted file mode 100644 index 8268213da7c..00000000000 --- a/packages/toolpad-core/src/PageContainer/PageHeader.tsx +++ /dev/null @@ -1,147 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Breadcrumbs from '@mui/material/Breadcrumbs'; -import Link from '@mui/material/Link'; -import Stack from '@mui/material/Stack'; -import Typography from '@mui/material/Typography'; -import useSlotProps from '@mui/utils/useSlotProps'; -import { styled } from '@mui/material'; -import { Link as ToolpadLink } from '../shared/Link'; -import { getItemTitle } from '../shared/navigation'; -import { useActivePage } from '../useActivePage'; -import { PageHeaderToolbar, PageHeaderToolbarProps } from './PageHeaderToolbar'; -import type { Breadcrumb } from './PageContainer'; - -const PageContentHeader = styled('div')(({ theme }) => ({ - display: 'flex', - flexDirection: 'row', - justifyContent: 'space-between', - gap: theme.spacing(2), -})); - -export interface PageHeaderSlotProps { - toolbar: PageHeaderToolbarProps; -} - -export interface PageHeaderSlots { - /** - * The component that renders the actions toolbar. - * @default PageHeaderToolbar - */ - toolbar: React.ElementType; -} - -export interface PageHeaderProps { - /** - * The title of the page. Leave blank to use the active page title. - */ - title?: string; - /** - * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs. - */ - breadcrumbs?: Breadcrumb[]; - /** - * The components used for each slot inside. - */ - slots?: PageHeaderSlots; - /** - * The props used for each slot inside. - */ - slotProps?: PageHeaderSlotProps; -} - -/** - * A header component to provide a title and breadcrumbs for your pages. - * - * Demos: - * - * - [Page Container](https://mui.com/toolpad/core/react-page-container/) - * - * API: - * - * - [PageHeader API](https://mui.com/toolpad/core/api/page-header) - */ -function PageHeader(props: PageHeaderProps) { - const { breadcrumbs, title } = props; - - const activePage = useActivePage(); - - const resolvedBreadcrumbs = breadcrumbs ?? activePage?.breadcrumbs ?? []; - const resolvedTitle = title ?? activePage?.title ?? ''; - - const ToolbarComponent = props?.slots?.toolbar ?? PageHeaderToolbar; - const toolbarSlotProps = useSlotProps({ - elementType: ToolbarComponent, - ownerState: props, - externalSlotProps: props?.slotProps?.toolbar, - additionalProps: {}, - }); - - return ( - - - {resolvedBreadcrumbs - ? resolvedBreadcrumbs.map((item, index) => { - return item.path ? ( - - {getItemTitle(item)} - - ) : ( - - {getItemTitle(item)} - - ); - }) - : null} - - - - {resolvedTitle ? {resolvedTitle} : null} - - - - ); -} - -PageHeader.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * The breadcrumbs of the page. Leave blank to use the active page breadcrumbs. - */ - breadcrumbs: PropTypes.arrayOf( - PropTypes.shape({ - path: PropTypes.string, - title: PropTypes.string.isRequired, - }), - ), - /** - * The props used for each slot inside. - */ - slotProps: PropTypes.shape({ - toolbar: PropTypes.shape({ - children: PropTypes.node, - }).isRequired, - }), - /** - * The components used for each slot inside. - */ - slots: PropTypes.shape({ - toolbar: PropTypes.elementType, - }), - /** - * The title of the page. Leave blank to use the active page title. - */ - title: PropTypes.string, -} as any; - -export { PageHeader }; diff --git a/packages/toolpad-core/src/PageContainer/PageHeaderToolbar.test.tsx b/packages/toolpad-core/src/PageContainer/PageHeaderToolbar.test.tsx deleted file mode 100644 index 58b8c335b6c..00000000000 --- a/packages/toolpad-core/src/PageContainer/PageHeaderToolbar.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test } from 'vitest'; -import describeConformance from '@toolpad/utils/describeConformance'; -import { PageHeaderToolbar } from './PageHeaderToolbar'; - -describe('PageHeaderToolbar', () => { - describeConformance(, () => ({ - skip: ['themeDefaultProps'], - })); - - test('dummy test', () => {}); -}); diff --git a/packages/toolpad-core/src/PageContainer/PageHeaderToolbar.tsx b/packages/toolpad-core/src/PageContainer/PageHeaderToolbar.tsx deleted file mode 100644 index 49944454b14..00000000000 --- a/packages/toolpad-core/src/PageContainer/PageHeaderToolbar.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; -import * as React from 'react'; -import PropTypes from 'prop-types'; -import { styled } from '@mui/material'; - -const PageHeaderToolbarRoot = styled('div')(({ theme }) => ({ - display: 'flex', - flexDirection: 'row', - gap: theme.spacing(1), - // Ensure the toolbar is always on the right side, even after wrapping - marginLeft: 'auto', -})); - -export interface PageHeaderToolbarProps { - children?: React.ReactNode; -} - -/** - * - * Demos: - * - * - [Page Container](https://mui.com/toolpad/core/react-page-container/) - * - * API: - * - * - [PageHeaderToolbar API](https://mui.com/toolpad/core/api/page-header-toolbar) - */ -function PageHeaderToolbar(props: PageHeaderToolbarProps) { - return ; -} - -PageHeaderToolbar.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * @ignore - */ - children: PropTypes.node, -} as any; - -export { PageHeaderToolbar }; diff --git a/packages/toolpad-core/src/PageContainer/index.ts b/packages/toolpad-core/src/PageContainer/index.ts deleted file mode 100644 index c72edd9b0e2..00000000000 --- a/packages/toolpad-core/src/PageContainer/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './PageContainer'; -export * from './PageHeader'; -export * from './PageHeaderToolbar'; diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx deleted file mode 100644 index ccaa6186ce7..00000000000 --- a/packages/toolpad-core/src/SignInPage/SignInPage.test.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/// - -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import describeConformance from '@toolpad/utils/describeConformance'; -import { SignInPage } from './SignInPage'; - -describe('SignInPage', () => { - describeConformance(, () => ({ - skip: ['themeDefaultProps'], - })); - - test('renders oauth button', async () => { - const signIn = vi.fn(); - render(); - - const signInButton = screen.getByRole('button', { name: 'Sign in with GitHub' }); - - await userEvent.click(signInButton); - - expect(signIn).toHaveBeenCalledWith({ id: 'github', name: 'GitHub' }, undefined, '/'); - }); - - test('renders credentials provider', async () => { - const signIn = vi.fn(); - render(); - - const emailField = screen.getByRole('textbox', { name: 'Email' }); - const passwordField = screen.getByLabelText(/Password/); - const signInButton = screen.getByRole('button', { name: 'Sign in' }); - - await userEvent.type(emailField, 'john@example.com'); - await userEvent.type(passwordField, 'thepassword'); - await userEvent.click(signInButton); - - const expectedFormData = new FormData(); - expectedFormData.append('email', 'john@example.com'); - expectedFormData.append('password', 'thepassword'); - - expect(signIn).toHaveBeenCalledWith( - { id: 'credentials', name: 'Credentials' }, - expectedFormData, - '/', - ); - }); - - test('renders nodemailer provider', async () => { - const signIn = vi.fn(); - render(); - - const emailField = screen.getByRole('textbox', { name: 'Email' }); - const signInButton = screen.getByRole('button', { name: 'Sign in with Email' }); - - await userEvent.type(emailField, 'john@example.com'); - await userEvent.click(signInButton); - - const expectedFormData = new FormData(); - expectedFormData.append('email', 'john@example.com'); - - expect(signIn).toHaveBeenCalledWith({ id: 'nodemailer', name: 'Email' }, expectedFormData, '/'); - }); - - test('renders passkey sign-in option when available', async () => { - const signIn = vi.fn(); - - render(); - - const emailField = screen.getByRole('textbox', { name: 'Email' }); - const signInButton = screen.getByRole('button', { name: 'Sign in with Passkey' }); - - await userEvent.type(emailField, 'john@example.com'); - await userEvent.click(signInButton); - - const expectedFormData = new FormData(); - expectedFormData.append('email', 'john@example.com'); - - expect(signIn).toHaveBeenCalledWith({ id: 'passkey', name: 'Passkey' }, expectedFormData, '/'); - }); -}); diff --git a/packages/toolpad-core/src/SignInPage/SignInPage.tsx b/packages/toolpad-core/src/SignInPage/SignInPage.tsx deleted file mode 100644 index 021e692460a..00000000000 --- a/packages/toolpad-core/src/SignInPage/SignInPage.tsx +++ /dev/null @@ -1,805 +0,0 @@ -'use client'; - -import * as React from 'react'; -import PropTypes from 'prop-types'; -import Alert from '@mui/material/Alert'; -import Button, { ButtonProps } from '@mui/material/Button'; -import Box from '@mui/material/Box'; -import Stack from '@mui/material/Stack'; -import Checkbox from '@mui/material/Checkbox'; -import Container from '@mui/material/Container'; -import Divider from '@mui/material/Divider'; -import FormControlLabel, { FormControlLabelProps } from '@mui/material/FormControlLabel'; -import TextField, { TextFieldProps } from '@mui/material/TextField'; -import Typography from '@mui/material/Typography'; -import GitHubIcon from '@mui/icons-material/GitHub'; -import PasswordIcon from '@mui/icons-material/Password'; -import FingerprintIcon from '@mui/icons-material/Fingerprint'; -import AppleIcon from '@mui/icons-material/Apple'; -import { alpha, useTheme, SxProps, type Theme } from '@mui/material/styles'; -import { LinkProps } from '@mui/material/Link'; -import GoogleIcon from './icons/Google'; -import FacebookIcon from './icons/Facebook'; -import TwitterIcon from './icons/Twitter'; -import InstagramIcon from './icons/Instagram'; -import TikTokIcon from './icons/TikTok'; -import LinkedInIcon from './icons/LinkedIn'; -import SlackIcon from './icons/Slack'; -import SpotifyIcon from './icons/Spotify'; -import TwitchIcon from './icons/Twitch'; -import DiscordIcon from './icons/Discord'; -import LineIcon from './icons/Line'; -import Auth0Icon from './icons/Auth0'; -import MicrosoftEntraIdIcon from './icons/MicrosoftEntra'; -import CognitoIcon from './icons/Cognito'; -import GitLabIcon from './icons/GitLab'; -import KeycloakIcon from './icons/Keycloak'; -import OktaIcon from './icons/Okta'; -import FusionAuthIcon from './icons/FusionAuth'; -import { BrandingContext, RouterContext } from '../shared/context'; -import { useLocaleText, type LocaleText } from '../AppProvider/LocalizationProvider'; - -const mergeSlotSx = (defaultSx: SxProps, slotProps?: { sx?: SxProps }) => { - if (Array.isArray(slotProps?.sx)) { - return [defaultSx, ...slotProps.sx]; - } - - if (slotProps?.sx) { - return [defaultSx, slotProps?.sx]; - } - - return [defaultSx]; -}; - -const getCommonTextFieldProps = (theme: Theme, baseProps: TextFieldProps = {}): TextFieldProps => ({ - required: true, - fullWidth: true, - ...baseProps, - slotProps: { - ...baseProps.slotProps, - htmlInput: { - ...baseProps.slotProps?.htmlInput, - sx: mergeSlotSx( - { - paddingTop: theme.spacing(1), - paddingBottom: theme.spacing(1), - }, - typeof baseProps.slotProps?.htmlInput === 'function' ? {} : baseProps.slotProps?.htmlInput, - ), - }, - inputLabel: { - ...baseProps.slotProps?.inputLabel, - sx: mergeSlotSx( - { - lineHeight: theme.typography.pxToRem(12), - fontSize: theme.typography.pxToRem(14), - }, - typeof baseProps.slotProps?.inputLabel === 'function' - ? {} - : baseProps.slotProps?.inputLabel, - ), - }, - }, -}); - -type SupportedOAuthProvider = - | 'github' - | 'google' - | 'facebook' - | 'gitlab' - | 'twitter' - | 'apple' - | 'instagram' - | 'tiktok' - | 'linkedin' - | 'slack' - | 'spotify' - | 'twitch' - | 'discord' - | 'line' - | 'auth0' - | 'cognito' - | 'keycloak' - | 'okta' - | 'fusionauth' - | 'microsoft-entra-id'; - -export type SupportedAuthProvider = - | SupportedOAuthProvider - | 'credentials' - | 'passkey' - | 'nodemailer' - | string; - -const IconProviderMap = new Map([ - ['github', ], - ['credentials', ], - ['google', ], - ['facebook', ], - ['passkey', ], - ['twitter', ], - ['apple', ], - ['instagram', ], - ['tiktok', ], - ['linkedin', ], - ['slack', ], - ['spotify', ], - ['twitch', ], - ['discord', ], - ['line', ], - ['auth0', ], - ['microsoft-entra-id', ], - ['cognito', ], - ['gitlab', ], - ['keycloak', ], - ['okta', ], - ['fusionauth', ], -]); - -interface SignInPageLocaleText { - signInTitle: string; - signInSubtitle: string; - signInRememberMe: string; - email: string; - password: string; - or: string; - with: string; - passkey: string; - to: string; -} - -export interface AuthProvider { - /** - * The unique identifier of the authentication provider. - * @default undefined - * @example 'google' - * @example 'github' - */ - id: SupportedAuthProvider; - /** - * The name of the authentication provider. - * @default '' - * @example 'Google' - * @example 'GitHub' - */ - name: string; -} - -export interface AuthResponse { - /** - * The error message if the sign-in failed. - * @default '' - */ - error?: string; - /** - * The type of error if the sign-in failed. - * @default '' - */ - type?: string; - /** - * The success notification if the sign-in was successful. - * @default '' - * Only used for magic link sign-in. - * @example 'Check your email for a magic link.' - */ - success?: string; -} - -export interface SignInPageSlots { - /** - * The custom email field component used in the credentials form. - * @default TextField - */ - emailField?: React.JSXElementConstructor; - /** - * The custom password field component used in the credentials form. - * @default TextField - */ - passwordField?: React.JSXElementConstructor; - /** - * The custom submit button component used in the credentials form. - * @default Button - */ - submitButton?: React.JSXElementConstructor; - /** - * The custom forgot password link component used in the credentials form. - * @default Link - */ - forgotPasswordLink?: React.JSXElementConstructor; - /** - * The custom sign up link component used in the credentials form. - * @default Link - */ - signUpLink?: React.JSXElementConstructor; - /** - * A component to override the default title section - * @default Typography - */ - title?: React.ElementType; - /** - * A component to override the default subtitle section - * @default Typography - */ - subtitle?: React.ElementType; - /** - * A component to override the default "Remember me" checkbox in the Credentials form - * @default FormControlLabel - */ - rememberMe?: React.ElementType; -} - -export interface SignInPageProps { - /** - * The list of authentication providers to display. - * @default [] - */ - providers?: AuthProvider[]; - /** - * Callback fired when a user signs in. - * @param {AuthProvider} provider The authentication provider. - * @param {FormData} formData The form data if the provider id is 'credentials'.\ - * @param {string} callbackUrl The URL to redirect to after signing in. - * @returns {void|Promise} - * @default undefined - */ - signIn?: ( - provider: AuthProvider, - formData?: any, - callbackUrl?: string, - ) => void | Promise | undefined; - /** - * The components used for each slot inside. - * @default {} - * @example { forgotPasswordLink: Forgot password? } - * @example { signUpLink: Sign up } - */ - slots?: SignInPageSlots; - /** - * The props used for each slot inside. - * @default {} - * @example { emailField: { autoFocus: false } } - * @example { passwordField: { variant: 'outlined' } } - * @example { emailField: { autoFocus: false }, passwordField: { variant: 'outlined' } } - */ - slotProps?: { - emailField?: TextFieldProps; - passwordField?: TextFieldProps; - submitButton?: ButtonProps; - forgotPasswordLink?: LinkProps; - signUpLink?: LinkProps; - rememberMe?: Partial; - form?: Partial>; - oAuthButton?: ButtonProps; - }; - /** - * The prop used to customize the styles on the `SignInPage` container - */ - sx?: SxProps; - /** - * The labels for the account component. - */ - localeText?: Partial; -} - -const defaultLocaleText: Pick = { - signInTitle: 'Sign in', - signInSubtitle: 'Please sign in to continue', - signInRememberMe: 'Remember me', - email: 'Email', - password: 'Password', - or: 'or', - with: 'with', - passkey: 'Passkey', - to: 'to', -}; - -/** - * - * Demos: - * - * - [Sign-in Page](https://mui.com/toolpad/core/react-sign-in-page/) - * - * API: - * - * - [SignInPage API](https://mui.com/toolpad/core/api/sign-in-page) - */ -function SignInPage(props: SignInPageProps) { - const { providers, signIn, slots, slotProps, sx, localeText: propsLocaleText } = props; - const theme = useTheme(); - const branding = React.useContext(BrandingContext); - const router = React.useContext(RouterContext); - const globalLocaleText = useLocaleText(); - const localeText = { ...defaultLocaleText, ...globalLocaleText, ...propsLocaleText }; - - const passkeyProvider = providers?.find((provider) => provider.id === 'passkey'); - const credentialsProvider = providers?.find((provider) => provider.id === 'credentials'); - const emailProvider = providers?.find((provider) => provider.id === 'nodemailer'); - const [{ loading, selectedProviderId, error, success }, setFormStatus] = React.useState<{ - loading: boolean; - selectedProviderId?: SupportedAuthProvider; - error?: string; - success?: string; - }>({ - selectedProviderId: undefined, - loading: false, - error: '', - success: '', - }); - - const callbackUrl = router?.searchParams.get('callbackUrl') ?? '/'; - const singleProvider = React.useMemo(() => providers?.length === 1, [providers]); - const isOauthProvider = React.useCallback( - (provider?: SupportedAuthProvider) => - provider && provider !== 'credentials' && provider !== 'nodemailer' && provider !== 'passkey', - [], - ); - - return ( - - - - {branding?.logo} - - {slots?.title ? ( - - ) : ( - - {localeText.signInTitle}{' '} - {branding?.title ? `${localeText.to?.toLocaleLowerCase()} ${branding.title}` : null} - - )} - {slots?.subtitle ? ( - - ) : ( - - {localeText?.signInSubtitle} - - )} - - - {error && isOauthProvider(selectedProviderId) ? ( - {error} - ) : null} - {Object.values(providers ?? {}) - .filter((provider) => isOauthProvider(provider.id)) - .map((provider: AuthProvider) => { - return ( -
{ - event.preventDefault(); - setFormStatus({ - error: '', - selectedProviderId: provider.id, - loading: true, - }); - const oauthResponse = await signIn?.(provider, undefined, callbackUrl); - setFormStatus((prev) => ({ - ...prev, - loading: oauthResponse?.error ? false : prev.loading, - error: oauthResponse?.error, - })); - }} - {...slotProps?.form} - > - -
- ); - })} -
- - {passkeyProvider ? ( - - {singleProvider ? null : ( - {localeText.or} - )} - {error && selectedProviderId === 'passkey' ? ( - - {error} - - ) : null} - { - setFormStatus({ - error: '', - selectedProviderId: passkeyProvider.id, - loading: true, - }); - event.preventDefault(); - const formData = new FormData(event.currentTarget); - const passkeyResponse = await signIn?.(passkeyProvider, formData, callbackUrl); - setFormStatus((prev) => ({ - ...prev, - loading: false, - error: passkeyResponse?.error, - })); - }} - {...slotProps?.form} - > - {slots?.emailField ? ( - - ) : ( - - )} - {slots?.submitButton ? ( - - ) : ( - - )} - - - ) : null} - - {emailProvider ? ( - - {singleProvider ? null : ( - {localeText.or} - )} - {error && selectedProviderId === 'nodemailer' ? ( - - {error} - - ) : null} - {success && selectedProviderId === 'nodemailer' ? ( - - {success} - - ) : null} - { - event.preventDefault(); - setFormStatus({ - error: '', - selectedProviderId: emailProvider.id, - loading: true, - }); - const formData = new FormData(event.currentTarget); - const emailResponse = await signIn?.(emailProvider, formData, callbackUrl); - setFormStatus((prev) => ({ - ...prev, - loading: false, - error: emailResponse?.error, - success: emailResponse?.success, - })); - }} - {...slotProps?.form} - > - {slots?.emailField ? ( - - ) : ( - - )} - {slots?.submitButton ? ( - - ) : ( - - )} - - - ) : null} - - {credentialsProvider ? ( - - {singleProvider ? null : ( - {localeText.or} - )} - {error && selectedProviderId === 'credentials' ? ( - - {error} - - ) : null} - { - setFormStatus({ - error: '', - selectedProviderId: credentialsProvider.id, - loading: true, - }); - event.preventDefault(); - const formData = new FormData(event.currentTarget); - const credentialsResponse = await signIn?.( - credentialsProvider, - formData, - callbackUrl, - ); - setFormStatus((prev) => ({ - ...prev, - loading: false, - error: credentialsResponse?.error, - })); - }} - {...slotProps?.form} - > - - {slots?.emailField ? ( - - ) : ( - - )} - {slots?.passwordField ? ( - - ) : ( - - )} - - - {slots?.rememberMe ? ( - - ) : ( - - } - label={localeText.signInRememberMe} - {...slotProps?.rememberMe} - slotProps={{ - typography: { - color: 'textSecondary', - fontSize: theme.typography.pxToRem(14), - }, - ...slotProps?.rememberMe?.slotProps, - }} - /> - )} - {slots?.forgotPasswordLink ? ( - - ) : null} - - {slots?.submitButton ? ( - - ) : ( - - )} - - {slots?.signUpLink ? ( - - {slots?.signUpLink ? : null} - - ) : null} - - - ) : null} -
-
-
-
- ); -} - -SignInPage.propTypes /* remove-proptypes */ = { - // ┌────────────────────────────── Warning ──────────────────────────────┐ - // │ These PropTypes are generated from the TypeScript type definitions. │ - // │ To update them, edit the TypeScript types and run `pnpm proptypes`. │ - // └─────────────────────────────────────────────────────────────────────┘ - /** - * The labels for the account component. - */ - localeText: PropTypes.object, - /** - * The list of authentication providers to display. - * @default [] - */ - providers: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - }), - ), - /** - * Callback fired when a user signs in. - * @param {AuthProvider} provider The authentication provider. - * @param {FormData} formData The form data if the provider id is 'credentials'.\ - * @param {string} callbackUrl The URL to redirect to after signing in. - * @returns {void|Promise} - * @default undefined - */ - signIn: PropTypes.func, - /** - * The props used for each slot inside. - * @default {} - * @example { emailField: { autoFocus: false } } - * @example { passwordField: { variant: 'outlined' } } - * @example { emailField: { autoFocus: false }, passwordField: { variant: 'outlined' } } - */ - slotProps: PropTypes.shape({ - emailField: PropTypes.object, - forgotPasswordLink: PropTypes.object, - form: PropTypes.object, - oAuthButton: PropTypes.object, - passwordField: PropTypes.object, - rememberMe: PropTypes.object, - signUpLink: PropTypes.object, - submitButton: PropTypes.object, - }), - /** - * The components used for each slot inside. - * @default {} - * @example { forgotPasswordLink: Forgot password? } - * @example { signUpLink: Sign up } - */ - slots: PropTypes.shape({ - emailField: PropTypes.elementType, - forgotPasswordLink: PropTypes.elementType, - passwordField: PropTypes.elementType, - rememberMe: PropTypes.elementType, - signUpLink: PropTypes.elementType, - submitButton: PropTypes.elementType, - subtitle: PropTypes.elementType, - title: PropTypes.elementType, - }), - /** - * The prop used to customize the styles on the `SignInPage` container - */ - sx: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.func, PropTypes.object, PropTypes.bool])), - PropTypes.func, - PropTypes.object, - ]), -} as any; - -export { SignInPage }; diff --git a/packages/toolpad-core/src/SignInPage/icons/Auth0.tsx b/packages/toolpad-core/src/SignInPage/icons/Auth0.tsx deleted file mode 100644 index e5ed436b504..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Auth0.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ -function Auth0Icon() { - return ( - - - - ); -} - -export default Auth0Icon; diff --git a/packages/toolpad-core/src/SignInPage/icons/Cognito.tsx b/packages/toolpad-core/src/SignInPage/icons/Cognito.tsx deleted file mode 100644 index 0d309ff55d1..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Cognito.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function CognitoIcon() { - return ( - - - - - - - - - - - ); -} -export default CognitoIcon; diff --git a/packages/toolpad-core/src/SignInPage/icons/Discord.tsx b/packages/toolpad-core/src/SignInPage/icons/Discord.tsx deleted file mode 100644 index dd5a21744f5..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Discord.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function DiscordIcon() { - return ( - - - - ); -} - -export default DiscordIcon; diff --git a/packages/toolpad-core/src/SignInPage/icons/Facebook.tsx b/packages/toolpad-core/src/SignInPage/icons/Facebook.tsx deleted file mode 100644 index 7a4898959a2..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Facebook.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function FacebookIcon() { - return ( - - - - - ); -} - -export default FacebookIcon; diff --git a/packages/toolpad-core/src/SignInPage/icons/FusionAuth.tsx b/packages/toolpad-core/src/SignInPage/icons/FusionAuth.tsx deleted file mode 100644 index 1775b80d24b..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/FusionAuth.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function FusionAuthIcon() { - return ( - - - - - - - - - - - - - - - ); -} - -export default FusionAuthIcon; diff --git a/packages/toolpad-core/src/SignInPage/icons/GitLab.tsx b/packages/toolpad-core/src/SignInPage/icons/GitLab.tsx deleted file mode 100644 index 39c12f141c8..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/GitLab.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -export default function GitLabIcon() { - return ( - - - - - - - ); -} diff --git a/packages/toolpad-core/src/SignInPage/icons/Google.tsx b/packages/toolpad-core/src/SignInPage/icons/Google.tsx deleted file mode 100644 index d12cd27435b..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Google.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function GoogleIcon() { - return ( - - - - - - - - ); -} - -export default GoogleIcon; diff --git a/packages/toolpad-core/src/SignInPage/icons/Instagram.tsx b/packages/toolpad-core/src/SignInPage/icons/Instagram.tsx deleted file mode 100644 index 7ade657a6af..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Instagram.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -export default function InstagramIcon() { - return ( - - - - - - - - - - - - - - - - ); -} diff --git a/packages/toolpad-core/src/SignInPage/icons/Keycloak.tsx b/packages/toolpad-core/src/SignInPage/icons/Keycloak.tsx deleted file mode 100644 index 96155ec9a52..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Keycloak.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -export default function KeycloakIcon() { - return ( - - - - - ); -} diff --git a/packages/toolpad-core/src/SignInPage/icons/Line.tsx b/packages/toolpad-core/src/SignInPage/icons/Line.tsx deleted file mode 100644 index 0b08f03886c..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Line.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -export default function LineIcon() { - return ( - - - - - - ); -} diff --git a/packages/toolpad-core/src/SignInPage/icons/LinkedIn.tsx b/packages/toolpad-core/src/SignInPage/icons/LinkedIn.tsx deleted file mode 100644 index fdbd5b6ad2f..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/LinkedIn.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function LinkedInIcon() { - return ( - - - - - - ); -} - -export default LinkedInIcon; diff --git a/packages/toolpad-core/src/SignInPage/icons/MicrosoftEntra.tsx b/packages/toolpad-core/src/SignInPage/icons/MicrosoftEntra.tsx deleted file mode 100644 index 062da229f47..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/MicrosoftEntra.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -export default function MicrosoftEntraIdIcon() { - return ( - - - - - - - - ); -} diff --git a/packages/toolpad-core/src/SignInPage/icons/Okta.tsx b/packages/toolpad-core/src/SignInPage/icons/Okta.tsx deleted file mode 100644 index a1cc9663d23..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Okta.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -export default function OktaIcon() { - return ( - - - - ); -} diff --git a/packages/toolpad-core/src/SignInPage/icons/Slack.tsx b/packages/toolpad-core/src/SignInPage/icons/Slack.tsx deleted file mode 100644 index b962839e05e..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Slack.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -export default function SlackIcon() { - return ( - - - - - - - - - ); -} diff --git a/packages/toolpad-core/src/SignInPage/icons/Spotify.tsx b/packages/toolpad-core/src/SignInPage/icons/Spotify.tsx deleted file mode 100644 index e9e77628426..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Spotify.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function SpotifyIcon() { - return ( - - - - ); -} - -export default SpotifyIcon; diff --git a/packages/toolpad-core/src/SignInPage/icons/TikTok.tsx b/packages/toolpad-core/src/SignInPage/icons/TikTok.tsx deleted file mode 100644 index 0172aa3a375..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/TikTok.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function TikTokIcon() { - return ( - - - - - - - - - - ); -} - -export default TikTokIcon; diff --git a/packages/toolpad-core/src/SignInPage/icons/Twitch.tsx b/packages/toolpad-core/src/SignInPage/icons/Twitch.tsx deleted file mode 100644 index 876bf01714d..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Twitch.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function TwitchIcon() { - return ( - - - - - ); -} - -export default TwitchIcon; diff --git a/packages/toolpad-core/src/SignInPage/icons/Twitter.tsx b/packages/toolpad-core/src/SignInPage/icons/Twitter.tsx deleted file mode 100644 index 6c8dfe24bbc..00000000000 --- a/packages/toolpad-core/src/SignInPage/icons/Twitter.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import * as React from 'react'; - -/** - * @ignore - internal component. - */ - -function TwitterIcon() { - return ( - - - - ); -} - -export default TwitterIcon; diff --git a/packages/toolpad-core/src/SignInPage/index.ts b/packages/toolpad-core/src/SignInPage/index.ts deleted file mode 100644 index aa485859b44..00000000000 --- a/packages/toolpad-core/src/SignInPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './SignInPage'; diff --git a/packages/toolpad-core/src/index.ts b/packages/toolpad-core/src/index.ts deleted file mode 100644 index e40b9ada6ce..00000000000 --- a/packages/toolpad-core/src/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export * from './AppProvider'; - -export * from './DashboardLayout'; - -export * from './SignInPage'; - -export * from './Account'; - -export * from './PageContainer'; - -export * from './Crud'; - -export * from './useActivePage'; - -export * from './useDialogs'; - -export * from './useNotifications'; - -export * from './useLocalStorageState'; - -export * from './useSession'; - -export * from './useSessionStorageState'; - -export * from './persistence/codec'; diff --git a/packages/toolpad-core/src/internal/context.ts b/packages/toolpad-core/src/internal/context.ts deleted file mode 100644 index f3edbe55743..00000000000 --- a/packages/toolpad-core/src/internal/context.ts +++ /dev/null @@ -1,4 +0,0 @@ -'use client'; -import * as React from 'react'; - -export const DocsContext = React.createContext(false); diff --git a/packages/toolpad-core/src/internal/demo.tsx b/packages/toolpad-core/src/internal/demo.tsx deleted file mode 100644 index 04e542cb3e0..00000000000 --- a/packages/toolpad-core/src/internal/demo.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; -import * as React from 'react'; -import { Router } from '../AppProvider'; - -/** - * Internal utility for demos - * @ignore - internal component. - */ - -const DUMMY_BASE = 'https://example.com'; - -/** - * Hook to create a router for demos. - * @returns An in-memory router To be used in demos demos. - */ -export function useDemoRouter(initialUrl: string = '/') { - const [url, setUrl] = React.useState(() => new URL(initialUrl, DUMMY_BASE)); - - const router = React.useMemo(() => { - return { - pathname: url.pathname, - searchParams: url.searchParams, - navigate: (newUrl) => { - const nextUrl = new URL(newUrl, DUMMY_BASE); - if (nextUrl.pathname !== url.pathname || nextUrl.search !== url.search) { - setUrl(nextUrl); - } - }, - }; - }, [url.pathname, url.search, url.searchParams]); - - return router; -} diff --git a/packages/toolpad-core/src/internal/index.tsx b/packages/toolpad-core/src/internal/index.tsx deleted file mode 100644 index e3391b5e1a2..00000000000 --- a/packages/toolpad-core/src/internal/index.tsx +++ /dev/null @@ -1,4 +0,0 @@ -export { Link } from '../shared/Link'; - -export * from './demo'; -export { DocsContext } from './context'; diff --git a/packages/toolpad-core/src/locales/en.tsx b/packages/toolpad-core/src/locales/en.tsx deleted file mode 100644 index 47d5938e5a2..00000000000 --- a/packages/toolpad-core/src/locales/en.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import type { LocaleText } from '../AppProvider'; -import { getLocalization } from './getLocalization'; - -const en: LocaleText = { - // Account - accountSignInLabel: 'Sign In', - accountSignOutLabel: 'Sign Out', - - // AccountPreview - accountPreviewTitle: 'Account', - accountPreviewIconButtonLabel: 'Current User', - - // SignInPage - signInTitle: 'Sign In', - signInSubtitle: 'Welcome user, please sign in to continue', - signInRememberMe: 'Remember Me', - oauthSignInTitle: 'Sign in with OAuth', - passkeySignInTitle: 'Sign in with Passkey', - magicLinkSignInTitle: 'Sign in with Magic Link', - - // Common authentication labels - email: 'Email', - password: 'Password', - username: 'Username', - passkey: 'Passkey', - - // Common action labels - save: 'Save', - cancel: 'Cancel', - ok: 'Ok', - or: 'Or', - to: 'To', - with: 'With', - close: 'Close', - delete: 'Delete', - alert: 'Alert', - confirm: 'Confirm', - loading: 'Loading...', - - // CRUD - createNewButtonLabel: 'Create new', - reloadButtonLabel: 'Reload data', - createLabel: 'Create', - createSuccessMessage: 'Item created successfully.', - createErrorMessage: 'Failed to create item. Reason:', - editLabel: 'Edit', - editSuccessMessage: 'Item edited successfully.', - editErrorMessage: 'Failed to edit item. Reason:', - deleteLabel: 'Delete', - deleteConfirmTitle: 'Delete item?', - deleteConfirmMessage: 'Do you wish to delete this item?', - deleteConfirmLabel: 'Delete', - deleteCancelLabel: 'Cancel', - deleteSuccessMessage: 'Item deleted successfully.', - deleteErrorMessage: 'Failed to delete item. Reason:', - deletedItemMessage: 'This item has been deleted.', -}; - -export default getLocalization(en); diff --git a/packages/toolpad-core/src/locales/getLocalization.ts b/packages/toolpad-core/src/locales/getLocalization.ts deleted file mode 100644 index a99a3b5d418..00000000000 --- a/packages/toolpad-core/src/locales/getLocalization.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { LocaleText } from '../AppProvider'; - -export const getLocalization = (translations: Partial) => { - return { - components: { - MuiLocalizationProvider: { - defaultProps: { - localeText: { ...translations }, - }, - }, - }, - }; -}; diff --git a/packages/toolpad-core/src/locales/hiIN.tsx b/packages/toolpad-core/src/locales/hiIN.tsx deleted file mode 100644 index 16175c45125..00000000000 --- a/packages/toolpad-core/src/locales/hiIN.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import type { LocaleText } from '../AppProvider'; -import { getLocalization } from './getLocalization'; - -const hiINLabels: Partial = { - // Account - accountSignInLabel: 'साइन इन करें', - accountSignOutLabel: 'साइन आउट करें', - - // AccountPreview - accountPreviewTitle: 'खाता', - accountPreviewIconButtonLabel: 'वर्तमान उपयोगकर्ता', - - // SignInPage - signInTitle: 'साइन इन करें', - signInSubtitle: 'स्वागत है उपयोगकर्ता, कृपया जारी रखने के लिए साइन इन करें', - signInRememberMe: 'मुझे याद रखें', - oauthSignInTitle: 'साइन इन विकल्प', - passkeySignInTitle: 'साइन इन विकल्प', - magicLinkSignInTitle: 'साइन इन विकल्प', - - // Common authentication labels - email: 'ईमेल', - password: 'पासवर्ड', - username: 'उपयोगकर्ता नाम', - passkey: 'पासकी', - - // Common action labels - save: 'सहेजें', - cancel: 'रद्द करें', - ok: 'ठीक है', - or: 'या', - to: 'को', - with: 'के साथ', - close: 'बंद करें', - delete: 'हटाएं', - alert: 'सूचना', - confirm: 'पुष्टि करें', - loading: 'लोड हो रहा है...', -}; - -export default getLocalization(hiINLabels); diff --git a/packages/toolpad-core/src/nextjs/NextAppProvider.test.tsx b/packages/toolpad-core/src/nextjs/NextAppProvider.test.tsx deleted file mode 100644 index 05b54e87833..00000000000 --- a/packages/toolpad-core/src/nextjs/NextAppProvider.test.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test, expect, vi } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import { NextAppProvider } from './NextAppProvider'; -import { Router } from '../AppProvider'; - -vi.mock('next/navigation.js', () => { - const searchParams = new URLSearchParams(); - const push = () => {}; - const replace = () => {}; - const router = { push, replace }; - return { - usePathname: () => '/', - useSearchParams: () => searchParams, - useRouter: () => router, - }; -}); - -vi.mock('next/router.js', () => ({ useRouter: () => null })); - -vi.mock('next/compat/router.js', () => ({ useRouter: () => null })); - -vi.mock('next/link.js', () => ({ default: () => null })); - -interface RouterTestProps { - children: React.ReactNode; -} - -function RouterTest({ children }: RouterTestProps) { - const [pathname, setPathname] = React.useState('/page'); - - const router = React.useMemo(() => { - return { - pathname, - searchParams: new URLSearchParams(), - navigate: (path) => setPathname(String(path)), - }; - }, [pathname]); - - return {children}; -} - -describe('Nextjs AppProvider', () => { - test('renders content correctly', async () => { - // placeholder test - render(Hello); - - expect(screen.getByText('Hello')).toBeTruthy(); - }); -}); diff --git a/packages/toolpad-core/src/nextjs/NextAppProvider.tsx b/packages/toolpad-core/src/nextjs/NextAppProvider.tsx deleted file mode 100644 index cbdea5b579e..00000000000 --- a/packages/toolpad-core/src/nextjs/NextAppProvider.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useRouter } from 'next/compat/router.js'; -import { NextAppProviderApp } from './NextAppProviderApp'; -import { NextAppProviderPages } from './NextAppProviderPages'; -import type { AppProviderProps } from '../AppProvider'; - -function NextAppProvider(props: AppProviderProps) { - const router = useRouter(); - const AppProvider = router ? NextAppProviderPages : NextAppProviderApp; - return ; -} - -export { NextAppProvider }; diff --git a/packages/toolpad-core/src/nextjs/NextAppProviderApp.tsx b/packages/toolpad-core/src/nextjs/NextAppProviderApp.tsx deleted file mode 100644 index d3b78c3ac3b..00000000000 --- a/packages/toolpad-core/src/nextjs/NextAppProviderApp.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from 'react'; -import NextLink from 'next/link.js'; -import { usePathname, useSearchParams, useRouter } from 'next/navigation.js'; -import { LinkProps } from '../shared/Link'; -import { AppProvider } from '../AppProvider'; -import type { AppProviderProps, Navigate, Router } from '../AppProvider'; - -const Link = React.forwardRef((props, ref) => { - const { href, history, ...rest } = props; - return ; -}); -/** - * @ignore - internal component. - */ -export function NextAppProviderApp(props: AppProviderProps) { - const pathname = usePathname(); - const searchParams = useSearchParams(); - const { push, replace } = useRouter(); - - const navigate = React.useCallback( - (url, { history = 'auto' } = {}) => { - if (history === 'auto' || history === 'push') { - return push(String(url)); - } - if (history === 'replace') { - return replace(String(url)); - } - throw new Error(`Invalid history option: ${history}`); - }, - [push, replace], - ); - - const routerImpl = React.useMemo( - () => ({ - pathname, - searchParams, - navigate, - Link, - }), - [pathname, navigate, searchParams], - ); - - return ; -} diff --git a/packages/toolpad-core/src/nextjs/NextAppProviderPages.tsx b/packages/toolpad-core/src/nextjs/NextAppProviderPages.tsx deleted file mode 100644 index 7ec2f5a0af3..00000000000 --- a/packages/toolpad-core/src/nextjs/NextAppProviderPages.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from 'react'; -import NextLink from 'next/link.js'; -import { asArray } from '@toolpad/utils/collections'; -import { useRouter } from 'next/router.js'; -import { LinkProps } from '../shared/Link'; -import { AppProvider } from '../AppProvider'; -import type { AppProviderProps, Navigate, Router } from '../AppProvider'; - -const Link = React.forwardRef((props, ref) => { - const { href, history, ...rest } = props; - return ; -}); - -/** - * @ignore - internal component. - */ -export function NextAppProviderPages(props: AppProviderProps) { - const { push, replace, asPath, query } = useRouter(); - - const search = React.useMemo(() => { - const params = new URLSearchParams(); - Object.entries(query ?? {}).forEach(([key, value]) => { - asArray(value ?? []).forEach((v) => { - params.append(key, v); - }); - }); - return params.toString(); - }, [query]); - - // Stable search params object - const searchParams = React.useMemo(() => new URLSearchParams(search), [search]); - - const navigate = React.useCallback( - (url, { history = 'auto' } = {}) => { - if (history === 'auto' || history === 'push') { - return push(String(url)); - } - if (history === 'replace') { - return replace(String(url)); - } - throw new Error(`Invalid history option: ${history}`); - }, - [push, replace], - ); - - const routerImpl = React.useMemo( - () => ({ - pathname: asPath.split('?')[0], - searchParams, - navigate, - Link, - }), - [asPath, navigate, searchParams], - ); - - return ; -} diff --git a/packages/toolpad-core/src/nextjs/index.tsx b/packages/toolpad-core/src/nextjs/index.tsx deleted file mode 100644 index c1b8b9cdcbf..00000000000 --- a/packages/toolpad-core/src/nextjs/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './NextAppProvider'; diff --git a/packages/toolpad-core/src/persistence/codec.tsx b/packages/toolpad-core/src/persistence/codec.tsx deleted file mode 100644 index f165482ea4c..00000000000 --- a/packages/toolpad-core/src/persistence/codec.tsx +++ /dev/null @@ -1,81 +0,0 @@ -/** - * A codec that can encode and decode values of type V to and from strings. - * @typeParam V The type of values that can be encoded and decoded. - */ -export interface Codec { - /** - * Decodes a string value into a value of type V. - * @param value The value to decode. - * @returns The decoded value. - */ - parse: (value: string) => V; - /** - * Encodes a value of type V into a string. - * @param value The value to encode. - * @returns The encoded value. - */ - stringify: (value: V) => string; -} - -/** - * A codec that can encode and decode Date objects to and from strings. - */ -export const CODEC_DATE: Codec = { - parse: (value) => new Date(value), - stringify: (value) => value.toISOString(), -}; - -/** - * A codec that can encode and decode Date objects to and from strings, but only the date part. - */ -export const CODEC_DATE_ONLY: Codec = { - parse: (value) => new Date(value), - stringify: (value) => value.toISOString().split('T')[0], -}; - -/** - * A codec that can encode and decode numbers to and from strings. - */ -export const CODEC_NUMBER: Codec = { - parse: (value) => Number(value), - stringify: (value) => String(value), -}; - -/** - * A codec that can encode and decode boolean values to and from strings. - */ -export const CODE_BOOLEAN: Codec = { - parse: (value) => value === 'true', - stringify: (value) => String(value), -}; - -/** - * A codec that can encode and decode JSON values to and from strings. - */ -export const CODEC_JSON: Codec = { - parse: (value) => { - try { - return JSON.parse(value); - } catch { - return null; - } - }, - stringify: (value) => JSON.stringify(value), -}; - -/** - * A codec that can encode and decode JSON values to and from strings. - * If the JSON value is invalid, parsing will fail. - */ -export const CODEC_JSON_STRICT: Codec = { - parse: (value) => JSON.parse(value), - stringify: (value) => JSON.stringify(value), -}; - -/** - * A codec that can encode and decode strings to and from strings. - */ -export const CODEC_STRING: Codec = { - parse: (value) => value, - stringify: (value) => value, -}; diff --git a/packages/toolpad-core/src/persistence/index.ts b/packages/toolpad-core/src/persistence/index.ts deleted file mode 100644 index 1abe4d96287..00000000000 --- a/packages/toolpad-core/src/persistence/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useStorageState'; diff --git a/packages/toolpad-core/src/persistence/useStorageState.test.tsx b/packages/toolpad-core/src/persistence/useStorageState.test.tsx deleted file mode 100644 index 386ed2c91b4..00000000000 --- a/packages/toolpad-core/src/persistence/useStorageState.test.tsx +++ /dev/null @@ -1,82 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import { describe, test, expect, beforeEach } from 'vitest'; -import { renderHook } from '@testing-library/react'; -import { useStorageState } from './useStorageState'; - -describe('useStorageState', () => { - beforeEach(() => { - window.localStorage.clear(); - }); - - test('can do basic local storage with initial value', async () => { - const { result, rerender } = renderHook(() => - useStorageState(window.localStorage, 'foo', 'bar'), - ); - - expect(result.current[0]).toBe('bar'); - result.current[1]('baz'); - - rerender(); - - expect(result.current[0]).toBe('baz'); - }); - - test('can do basic local storage without initial value', async () => { - const { result, rerender } = renderHook(() => useStorageState(window.localStorage, 'foo')); - - expect(result.current[0]).toBe(null); - result.current[1]('baz'); - - rerender(); - - expect(result.current[0]).toBe('baz'); - }); - - test('can clear storage value, and reset to intiial value', async () => { - const { result, rerender } = renderHook(() => - useStorageState(window.localStorage, 'foo', 'bar'), - ); - - result.current[1]('baz'); - - rerender(); - - expect(result.current[0]).toBe('baz'); - result.current[1](null); - - rerender(); - - expect(result.current[0]).toBe('bar'); - }); - - test('can clear storage value', async () => { - const { result, rerender } = renderHook(() => useStorageState(window.localStorage, 'foo')); - - result.current[1]('baz'); - - rerender(); - - expect(result.current[0]).toBe('baz'); - result.current[1](null); - - rerender(); - - expect(result.current[0]).toBe(null); - }); - - test('can handle complex types', async () => { - const { result, rerender } = renderHook(() => - useStorageState(window.localStorage, 'foo', { a: 1 }, { codec: JSON }), - ); - - expect(result.current[0]).toEqual({ a: 1 }); - result.current[1]({ b: 2 }); - - rerender(); - - expect(result.current[0]).toEqual({ b: 2 }); - }); -}); diff --git a/packages/toolpad-core/src/persistence/useStorageState.tsx b/packages/toolpad-core/src/persistence/useStorageState.tsx deleted file mode 100644 index 749f26d079c..00000000000 --- a/packages/toolpad-core/src/persistence/useStorageState.tsx +++ /dev/null @@ -1,237 +0,0 @@ -'use client'; -import * as React from 'react'; -import { CODEC_STRING, Codec } from './codec'; - -// storage events only work across tabs, we'll use an event emitter to announce within the current tab -const currentTabChangeListeners = new Map void>>(); - -function onCurrentTabStorageChange(key: string, handler: () => void) { - let listeners = currentTabChangeListeners.get(key); - - if (!listeners) { - listeners = new Set(); - currentTabChangeListeners.set(key, listeners); - } - - listeners.add(handler); -} - -function offCurrentTabStorageChange(key: string, handler: () => void) { - const listeners = currentTabChangeListeners.get(key); - if (!listeners) { - return; - } - - listeners.delete(handler); - - if (listeners.size === 0) { - currentTabChangeListeners.delete(key); - } -} - -function emitCurrentTabStorageChange(key: string) { - const listeners = currentTabChangeListeners.get(key); - if (listeners) { - listeners.forEach((listener) => listener()); - } -} - -if (typeof window !== 'undefined') { - const origSetItem = window.localStorage.setItem; - window.localStorage.setItem = function setItem(key, value) { - const result = origSetItem.call(this, key, value); - emitCurrentTabStorageChange(key); - return result; - }; -} - -function subscribe(area: Storage, key: string | null, callback: () => void): () => void { - if (!key) { - return () => {}; - } - const storageHandler = (event: StorageEvent) => { - if (event.storageArea === area && event.key === key) { - callback(); - } - }; - window.addEventListener('storage', storageHandler); - onCurrentTabStorageChange(key, callback); - return () => { - window.removeEventListener('storage', storageHandler); - offCurrentTabStorageChange(key, callback); - }; -} - -function getSnapshot(area: Storage, key: string | null): string | null { - if (!key) { - return null; - } - try { - return area.getItem(key); - } catch { - // ignore - // See https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage - return null; - } -} - -function setValue(area: Storage, key: string | null, value: string | null) { - if (!key) { - return; - } - try { - if (value === null) { - area.removeItem(key); - } else { - area.setItem(key, String(value)); - } - } catch { - // ignore - // See https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API/Using_the_Web_Storage_API#feature-detecting_localstorage - return; - } - emitCurrentTabStorageChange(key); -} - -export type StorageStateInitializer = () => T | null; - -export type UseStorageStateHookResult = [ - T | null, - React.Dispatch>, -]; - -const serverValue: UseStorageStateHookResult = [null, () => {}]; - -export function useStorageStateServer(): UseStorageStateHookResult { - return serverValue; -} - -export interface DefaultStorageStateoptions { - codec?: Codec; -} -export interface StorageStateOptions extends DefaultStorageStateoptions { - codec: Codec; -} - -function encode(codec: Codec, value: V | null): string | null { - return value === null ? null : codec.stringify(value); -} - -function decode(codec: Codec, value: string | null): V | null { - return value === null ? null : codec.parse(value); -} - -// Start with null for the hydration, and then switch to the actual value. -const getKeyServerSnapshot = () => null; - -/** - * Sync state to local storage so that it persists through a page refresh. Usage is - * similar to useState except we pass in a storage key so that we can default - * to that value on page load instead of the specified initial value. - * - * Since the storage API isn't available in server-rendering environments, we - * return null during SSR and hydration. - */ -export function useStorageState( - area: Storage, - key: string | null, - initializer?: string | null | StorageStateInitializer, - options?: DefaultStorageStateoptions, -): UseStorageStateHookResult; -export function useStorageState( - area: Storage, - key: string | null, - initializer: T | null | StorageStateInitializer, - options: StorageStateOptions, -): UseStorageStateHookResult; -export function useStorageState( - area: Storage, - key: string | null, - initializer: T | null | StorageStateInitializer = null, - options?: DefaultStorageStateoptions | StorageStateOptions, -): UseStorageStateHookResult { - const codec = (options?.codec ?? CODEC_STRING) as Codec; - - const [initialValue] = React.useState(initializer); - const encodedInitialValue = React.useMemo( - () => encode(codec, initialValue), - [codec, initialValue], - ); - - const subscribeKey = React.useCallback( - (callback: () => void) => subscribe(area, key, callback), - [area, key], - ); - - const getKeySnapshot = React.useCallback<() => string | null>( - () => getSnapshot(area, key) ?? encodedInitialValue, - [area, encodedInitialValue, key], - ); - - const encodedStoredValue = React.useSyncExternalStore( - subscribeKey, - getKeySnapshot, - getKeyServerSnapshot, - ); - - const storedValue = React.useMemo( - () => decode(codec, encodedStoredValue), - [codec, encodedStoredValue], - ); - - const setStoredValue = React.useCallback( - (value: React.SetStateAction) => { - const valueToStore = value instanceof Function ? value(storedValue) : value; - const encodedValueToStore = encode(codec, valueToStore); - setValue(area, key, encodedValueToStore); - }, - [area, codec, storedValue, key], - ); - - const [nonStoredValue, setNonStoredValue] = React.useState(initialValue); - - if (!key) { - return [nonStoredValue, setNonStoredValue]; - } - - return [storedValue, setStoredValue]; -} - -export interface UseStorageState { - /** - * Sync state to local or session storage so that it persists through a page refresh. Usage is - * similar to useState except we pass in a storage key that uniquely identifies the value. - * @param key The key to use for storing the value in local or session storage. - * @param initializer The initial value to use if the key is not present in storage. - * @param options Additional options for the storage state. - */ - ( - key: string | null, - initializer?: string | null | StorageStateInitializer, - options?: DefaultStorageStateoptions, - ): UseStorageStateHookResult; - /** - * Sync state to local or session storage so that it persists through a page refresh. Usage is - * similar to useState except we pass in a storage key that uniquely identifies the value. - * @param key The key to use for storing the value in local or session storage. - * @param initializer The initial value to use if the key is not present in storage. - * @param options Additional options for the storage state. - */ - ( - key: string | null, - initializer: T | null | StorageStateInitializer, - options: StorageStateOptions, - ): UseStorageStateHookResult; - /** - * Sync state to local or session storage so that it persists through a page refresh. Usage is - * similar to useState except we pass in a storage key that uniquely identifies the value. - * @param key The key to use for storing the value in local or session storage. - * @param initializer The initial value to use if the key is not present in storage. - * @param options Additional options for the storage state. - */ - ( - key: string | null, - initializer?: T | null | StorageStateInitializer, - options?: StorageStateOptions, - ): UseStorageStateHookResult; -} diff --git a/packages/toolpad-core/src/react-router/ReactRouterAppProvider.test.tsx b/packages/toolpad-core/src/react-router/ReactRouterAppProvider.test.tsx deleted file mode 100644 index a63e5959264..00000000000 --- a/packages/toolpad-core/src/react-router/ReactRouterAppProvider.test.tsx +++ /dev/null @@ -1,22 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test, expect } from 'vitest'; -import { render, screen } from '@testing-library/react'; -import { BrowserRouter } from 'react-router'; -import { ReactRouterAppProvider } from './ReactRouterAppProvider'; - -describe('React Router AppProvider', () => { - test('renders content correctly', async () => { - // placeholder test - render( - - Hello - , - ); - - expect(screen.getByText('Hello')).toBeTruthy(); - }); -}); diff --git a/packages/toolpad-core/src/react-router/ReactRouterAppProvider.tsx b/packages/toolpad-core/src/react-router/ReactRouterAppProvider.tsx deleted file mode 100644 index 457a52b9684..00000000000 --- a/packages/toolpad-core/src/react-router/ReactRouterAppProvider.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; -import * as React from 'react'; -import { useSearchParams, useLocation, useNavigate, Link as ReactRouterLink } from 'react-router'; -import { AppProvider, type AppProviderProps, Navigate, Router } from '../AppProvider/AppProvider'; -import { LinkProps } from '../shared/Link'; - -const Link = React.forwardRef((props, ref) => { - const { href, history, ...rest } = props; - return ; -}); - -function ReactRouterAppProvider(props: AppProviderProps) { - const { pathname } = useLocation(); - const [searchParams] = useSearchParams(); - const navigate = useNavigate(); - - const navigateImpl = React.useCallback( - (url, { history = 'auto' } = {}) => { - if (history === 'auto' || history === 'push') { - return navigate(url); - } - if (history === 'replace') { - return navigate(url, { replace: true }); - } - throw new Error(`Invalid history option: ${history}`); - }, - [navigate], - ); - - const routerImpl = React.useMemo( - () => ({ - pathname, - searchParams, - navigate: navigateImpl, - Link, - }), - [pathname, searchParams, navigateImpl], - ); - - return ; -} - -export { ReactRouterAppProvider }; diff --git a/packages/toolpad-core/src/react-router/index.tsx b/packages/toolpad-core/src/react-router/index.tsx deleted file mode 100644 index 24a972e324c..00000000000 --- a/packages/toolpad-core/src/react-router/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './ReactRouterAppProvider'; diff --git a/packages/toolpad-core/src/shared/Link.tsx b/packages/toolpad-core/src/shared/Link.tsx deleted file mode 100644 index 9c4814bf8dc..00000000000 --- a/packages/toolpad-core/src/shared/Link.tsx +++ /dev/null @@ -1,55 +0,0 @@ -import * as React from 'react'; -import { RouterContext } from './context'; - -/** - * @ignore - internal component. - */ - -export interface LinkProps extends React.AnchorHTMLAttributes { - /* - * "replace" will replace the history stack with the URL being navigated to - * "push" will push the URL being navigated to as a new entry onto the history stack - * "auto" is the default and the mimics the "push" behaviour - */ - history?: 'auto' | 'push' | 'replace'; - href: string; -} - -export const DefaultLink = React.forwardRef(function Link( - props: LinkProps, - ref: React.ForwardedRef, -) { - const { children, href, onClick, history, ...rest } = props; - const routerContext = React.useContext(RouterContext); - - const handleLinkClick = React.useMemo(() => { - if (!routerContext) { - return onClick; - } - return (event: React.MouseEvent) => { - event.preventDefault(); - const url = new URL(event.currentTarget.href); - routerContext.navigate(url.pathname, { history }); - onClick?.(event); - }; - }, [routerContext, onClick, history]); - - return ( - - {children} - - ); -}); - -export const Link = React.forwardRef(function Link( - props: LinkProps, - ref: React.ForwardedRef, -) { - const routerContext = React.useContext(RouterContext); - const LinkComponent = routerContext?.Link ?? DefaultLink; - return ( - - {props.children} - - ); -}); diff --git a/packages/toolpad-core/src/shared/branding.ts b/packages/toolpad-core/src/shared/branding.ts deleted file mode 100644 index 316f85c481f..00000000000 --- a/packages/toolpad-core/src/shared/branding.ts +++ /dev/null @@ -1,7 +0,0 @@ -import * as React from 'react'; -import { BrandingContext } from './context'; - -export function useApplicationTitle() { - const branding = React.useContext(BrandingContext); - return branding?.title ?? 'Toolpad'; -} diff --git a/packages/toolpad-core/src/shared/components.tsx b/packages/toolpad-core/src/shared/components.tsx deleted file mode 100644 index 3aeed599eb3..00000000000 --- a/packages/toolpad-core/src/shared/components.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from 'react'; -import { CircularProgress, Typography, styled } from '@mui/material'; -import ErrorIcon from '@mui/icons-material/Error'; - -const OverlayRoot = styled('div')(({ theme }) => ({ - position: 'absolute', - inset: '0 0 0 0', - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - justifyContent: 'center', - padding: theme.spacing(2), -})); - -export interface ErrorOverlayProps { - error?: unknown; -} - -export function ErrorOverlay({ error }: ErrorOverlayProps) { - return ( - - - Error - - {(error as any)?.message ?? 'Unknown error'} - - ); -} - -export function LoadingOverlay() { - return ( - - - - ); -} diff --git a/packages/toolpad-core/src/shared/context.ts b/packages/toolpad-core/src/shared/context.ts deleted file mode 100644 index 8436f990b2f..00000000000 --- a/packages/toolpad-core/src/shared/context.ts +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; -import * as React from 'react'; -import type { PaletteMode } from '@mui/material'; -import type { Branding, Navigation, Router } from '../AppProvider'; -import type { DataModel } from '../Crud'; -import type { CrudProviderProps } from '../Crud/CrudProvider'; -import type { DataSourceCache } from '../Crud/cache'; - -export const BrandingContext = React.createContext(null); - -export const NavigationContext = React.createContext([]); - -export const PaletteModeContext = React.createContext<{ - paletteMode: PaletteMode; - setPaletteMode: (mode: PaletteMode) => void; - isDualTheme: boolean; -}>({ - paletteMode: 'light', - setPaletteMode: () => {}, - isDualTheme: false, -}); - -export const RouterContext = React.createContext(null); - -export const WindowContext = React.createContext(undefined); - -export const CrudContext = React.createContext<{ - dataSource: CrudProviderProps['dataSource'] | null; - dataSourceCache: DataSourceCache | null; -}>({ - dataSource: null, - dataSourceCache: null, -}); diff --git a/packages/toolpad-core/src/shared/navigation.tsx b/packages/toolpad-core/src/shared/navigation.tsx deleted file mode 100644 index b959e292972..00000000000 --- a/packages/toolpad-core/src/shared/navigation.tsx +++ /dev/null @@ -1,154 +0,0 @@ -import { pathToRegexp } from 'path-to-regexp'; -import invariant from 'invariant'; -import type { - Navigation, - NavigationItem, - NavigationPageItem, - NavigationSubheaderItem, -} from '../AppProvider'; - -export const getItemKind = (item: NavigationItem) => item.kind ?? 'page'; - -export const isPageItem = (item: NavigationItem): item is NavigationPageItem => - getItemKind(item) === 'page'; - -export const getItemTitle = (item: NavigationPageItem | NavigationSubheaderItem) => { - return isPageItem(item) ? (item.title ?? item.segment ?? '') : item.title; -}; - -/** - * Builds a map of navigation page items to their respective paths. This map is used to quickly - * lookup the path of a navigation item. It will be cached for the lifetime of the navigation. - */ -function buildItemToPathMap(navigation: Navigation): Map { - const map = new Map(); - - const visit = (item: NavigationItem, base: string) => { - if (isPageItem(item)) { - // Append segment to base path. Make sure to always have an initial slash, and slashes between segments. - const path = - `${base.startsWith('/') ? base : `/${base}`}${base && base !== '/' && item.segment ? '/' : ''}${item.segment || ''}` || - '/'; - map.set(item, path); - if (item.children) { - for (const child of item.children) { - visit(child, path); - } - } - } - }; - - for (const item of navigation) { - visit(item, ''); - } - - return map; -} - -const itemToPathMapCache = new WeakMap>(); - -/** - * Gets the cached map of navigation page items to their respective paths. - */ -function getItemToPathMap(navigation: Navigation) { - let map = itemToPathMapCache.get(navigation); - if (!map) { - map = buildItemToPathMap(navigation); - itemToPathMapCache.set(navigation, map); - } - return map; -} - -/** - * Build a lookup map of paths to navigation items. This map is used to match paths against - * to find the active page. - */ -function buildItemLookup(navigation: Navigation) { - const map = new Map(); - const visit = (item: NavigationItem) => { - if (isPageItem(item)) { - const path = getItemPath(navigation, item); - if (map.has(path)) { - console.warn(`Duplicate path in navigation: ${path}`); - } - - map.set(path, item); - if (item.pattern) { - const basePath = item.segment ? path.slice(0, -item.segment.length) : path; - map.set(pathToRegexp(basePath + item.pattern), item); - } - if (item.children) { - for (const child of item.children) { - visit(child); - } - } - } - }; - for (const item of navigation) { - visit(item); - } - return map; -} -const itemLookupMapCache = new WeakMap>(); -function getItemLookup(navigation: Navigation) { - let map = itemLookupMapCache.get(navigation); - if (!map) { - map = buildItemLookup(navigation); - itemLookupMapCache.set(navigation, map); - } - return map; -} - -/** - * Matches a path against the navigation to find the active page. i.e. the page that should be - * marked as selected in the navigation. - */ -export function matchPath(navigation: Navigation, path: string): NavigationPageItem | null { - const lookup = getItemLookup(navigation); - - for (const [key, item] of lookup.entries()) { - if (typeof key === 'string' && key === path) { - return item; - } - if (key instanceof RegExp && key.test(path)) { - return item; - } - } - - return null; -} - -/** - * Gets the path for a specific navigation page item. - */ -export function getItemPath(navigation: Navigation, item: NavigationPageItem): string { - const map = getItemToPathMap(navigation); - const path = map.get(item); - invariant(path, `Item not found in navigation: ${item.title}`); - return path; -} - -/** - * Checks if a specific navigation page item has the active page as a child item. - */ -export function hasSelectedNavigationChildren( - navigation: Navigation, - item: NavigationPageItem, - activePagePath: string, -): boolean { - if (item.children) { - return item.children.some((nestedItem) => { - if (!isPageItem(nestedItem)) { - return false; - } - - if (nestedItem.children) { - return hasSelectedNavigationChildren(navigation, nestedItem, activePagePath); - } - - return activePagePath === getItemPath(navigation, nestedItem); - }); - } - - return false; -} diff --git a/packages/toolpad-core/src/useActivePage/index.ts b/packages/toolpad-core/src/useActivePage/index.ts deleted file mode 100644 index 52662dbd290..00000000000 --- a/packages/toolpad-core/src/useActivePage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useActivePage'; diff --git a/packages/toolpad-core/src/useActivePage/useActivePage.ts b/packages/toolpad-core/src/useActivePage/useActivePage.ts deleted file mode 100644 index 74665f7a518..00000000000 --- a/packages/toolpad-core/src/useActivePage/useActivePage.ts +++ /dev/null @@ -1,60 +0,0 @@ -'use client'; -import * as React from 'react'; -import { NavigationContext, RouterContext } from '../shared/context'; -import { getItemPath, getItemTitle, matchPath } from '../shared/navigation'; -import type { Breadcrumb } from '../PageContainer'; - -export interface ActivePage { - title: string; - path: string; - breadcrumbs: Breadcrumb[]; -} - -export function useActivePage(): ActivePage | null { - const navigationContext = React.useContext(NavigationContext); - const routerContext = React.useContext(RouterContext); - const pathname = routerContext?.pathname ?? '/'; - const activeItem = matchPath(navigationContext, pathname); - - const rootItem = matchPath(navigationContext, '/'); - - return React.useMemo(() => { - if (!activeItem) { - return null; - } - - const breadcrumbs: Breadcrumb[] = []; - - if (rootItem) { - breadcrumbs.push({ - title: getItemTitle(rootItem), - path: '/', - }); - } - - const segments = pathname.split('/').filter(Boolean); - let prefix = ''; - for (const segment of segments) { - const path = `${prefix}/${segment}`; - prefix = path; - const item = matchPath(navigationContext, path); - if (!item) { - continue; - } - const itemPath = getItemPath(navigationContext, item); - const lastCrumb = breadcrumbs[breadcrumbs.length - 1]; - if (lastCrumb?.path !== itemPath) { - breadcrumbs.push({ - title: getItemTitle(item), - path: itemPath, - }); - } - } - - return { - title: getItemTitle(activeItem), - path: getItemPath(navigationContext, activeItem), - breadcrumbs, - }; - }, [activeItem, rootItem, pathname, navigationContext]); -} diff --git a/packages/toolpad-core/src/useDialogs/DialogsContext.tsx b/packages/toolpad-core/src/useDialogs/DialogsContext.tsx deleted file mode 100644 index f3f4f857b2f..00000000000 --- a/packages/toolpad-core/src/useDialogs/DialogsContext.tsx +++ /dev/null @@ -1,12 +0,0 @@ -'use client'; -import * as React from 'react'; -import type { CloseDialog, OpenDialog } from './useDialogs'; - -/** - * @ignore - internal component. - */ - -export const DialogsContext = React.createContext<{ - open: OpenDialog; - close: CloseDialog; -} | null>(null); diff --git a/packages/toolpad-core/src/useDialogs/DialogsProvider.test.tsx b/packages/toolpad-core/src/useDialogs/DialogsProvider.test.tsx deleted file mode 100644 index 14a1e3cb822..00000000000 --- a/packages/toolpad-core/src/useDialogs/DialogsProvider.test.tsx +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test } from 'vitest'; -import describeConformance from '@toolpad/utils/describeConformance'; -import { DialogsProvider } from './DialogsProvider'; - -describe('DialogsProvider', () => { - describeConformance(, () => ({ - skip: ['themeDefaultProps'], - })); - - test('dummy test', () => {}); -}); diff --git a/packages/toolpad-core/src/useDialogs/DialogsProvider.tsx b/packages/toolpad-core/src/useDialogs/DialogsProvider.tsx deleted file mode 100644 index f0d3876b536..00000000000 --- a/packages/toolpad-core/src/useDialogs/DialogsProvider.tsx +++ /dev/null @@ -1,114 +0,0 @@ -'use client'; -import invariant from 'invariant'; -import * as React from 'react'; -import useEventCallback from '@mui/utils/useEventCallback'; -import { DialogsContext } from './DialogsContext'; -import type { DialogComponent, OpenDialog, OpenDialogOptions } from './useDialogs'; - -interface DialogStackEntry { - key: string; - open: boolean; - promise: Promise; - Component: DialogComponent; - payload: P; - onClose: (result: R) => Promise; - resolve: (result: R) => void; -} - -export interface DialogProviderProps { - children?: React.ReactNode; - unmountAfter?: number; -} - -/** - * Provider for Dialog stacks. The subtree of this component can use the `useDialogs` hook to - * access the dialogs API. The dialogs are rendered in the order they are requested. - * - * Demos: - * - * - [useDialogs](https://mui.com/toolpad/core/react-use-dialogs/) - * - * API: - * - * - [DialogsProvider API](https://mui.com/toolpad/core/api/dialogs-provider) - */ -function DialogsProvider(props: DialogProviderProps) { - const { children, unmountAfter = 1000 } = props; - const [stack, setStack] = React.useState[]>([]); - const keyPrefix = React.useId(); - const nextId = React.useRef(0); - - const requestDialog = useEventCallback(function open( - Component: DialogComponent, - payload: P, - options: OpenDialogOptions = {}, - ) { - const { onClose = async () => {} } = options; - let resolve: ((result: R) => void) | undefined; - const promise = new Promise((resolveImpl) => { - resolve = resolveImpl; - }); - invariant(resolve, 'resolve not set'); - - const key = `${keyPrefix}-${nextId.current}`; - nextId.current += 1; - - const newEntry: DialogStackEntry = { - key, - open: true, - promise, - Component, - payload, - onClose, - resolve, - }; - - setStack((prevStack) => [...prevStack, newEntry]); - return promise; - }); - - const closeDialogUi = useEventCallback(function closeDialogUi(dialog: Promise) { - setStack((prevStack) => - prevStack.map((entry) => (entry.promise === dialog ? { ...entry, open: false } : entry)), - ); - setTimeout(() => { - // wait for closing animation - setStack((prevStack) => prevStack.filter((entry) => entry.promise !== dialog)); - }, unmountAfter); - }); - - const closeDialog = useEventCallback(async function closeDialog( - dialog: Promise, - result: R, - ) { - const entryToClose = stack.find((entry) => entry.promise === dialog); - invariant(entryToClose, 'dialog not found'); - await entryToClose.onClose(result); - entryToClose.resolve(result); - closeDialogUi(dialog); - return dialog; - }); - - const contextValue = React.useMemo( - () => ({ open: requestDialog, close: closeDialog }), - [requestDialog, closeDialog], - ); - - return ( - - {children} - {stack.map(({ key, open, Component, payload, promise }) => ( - { - await closeDialog(promise, result); - }} - /> - ))} - - ); -} - -export { DialogsProvider }; diff --git a/packages/toolpad-core/src/useDialogs/index.ts b/packages/toolpad-core/src/useDialogs/index.ts deleted file mode 100644 index 6fdad438d37..00000000000 --- a/packages/toolpad-core/src/useDialogs/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './useDialogs'; -export * from './DialogsProvider'; diff --git a/packages/toolpad-core/src/useDialogs/useDialogs.test.tsx b/packages/toolpad-core/src/useDialogs/useDialogs.test.tsx deleted file mode 100644 index 1e2019280ef..00000000000 --- a/packages/toolpad-core/src/useDialogs/useDialogs.test.tsx +++ /dev/null @@ -1,224 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test, expect } from 'vitest'; -import { renderHook, within, screen } from '@testing-library/react'; -import { userEvent } from '@testing-library/user-event'; -import { DialogProps, useDialogs } from './useDialogs'; -import { DialogsProvider } from './DialogsProvider'; - -interface TestWrapperProps { - children: React.ReactNode; -} - -function TestWrapper({ children }: TestWrapperProps) { - return {children}; -} - -describe('useDialogs', () => { - describe('alert', () => { - test('can show and hide', async () => { - const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); - - const dialogResult = result.current.alert('Hello'); - - rerender(); - - const dialog = await screen.findByRole('dialog'); - - expect(within(dialog).getByText('Hello')).toBeTruthy(); - - await userEvent.click(within(dialog).getByRole('button', { name: 'Ok' })); - - rerender(); - - expect(await dialogResult).toBeUndefined(); - - expect(screen.queryByRole('dialog')).toBeFalsy(); - }); - }); - - describe('confirm', () => { - test('can show and confirm', async () => { - const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); - - const dialogResult = result.current.confirm('Hello'); - - rerender(); - - const dialog = await screen.findByRole('dialog'); - - expect(within(dialog).getByText('Hello')).toBeTruthy(); - - await userEvent.click(within(dialog).getByRole('button', { name: 'Ok' })); - - rerender(); - - expect(await dialogResult).toBe(true); - - expect(screen.queryByRole('dialog')).toBeFalsy(); - }); - - test('can show and cancel', async () => { - const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); - - const dialogResult = result.current.confirm('Hello'); - - rerender(); - - const dialog = await screen.findByRole('dialog'); - - expect(within(dialog).getByText('Hello')).toBeTruthy(); - - await userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); - - rerender(); - - expect(await dialogResult).toBe(false); - - expect(screen.queryByRole('dialog')).toBeFalsy(); - }); - }); - - describe('prompt', () => { - test('can show and take input', async () => { - const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); - - const dialogResult = result.current.prompt('Hello'); - - rerender(); - - const dialog = await screen.findByRole('dialog'); - - expect(within(dialog).getByText('Hello')).toBeTruthy(); - - await userEvent.keyboard('Hello, World!'); - - await userEvent.click(within(dialog).getByRole('button', { name: 'Ok' })); - - rerender(); - - expect(await dialogResult).toBe('Hello, World!'); - - expect(screen.queryByRole('dialog')).toBeFalsy(); - }); - - test('can show and cancel', async () => { - const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); - - const dialogResult = result.current.prompt('Hello'); - - rerender(); - - const dialog = await screen.findByRole('dialog'); - - expect(within(dialog).getByText('Hello')).toBeTruthy(); - - await userEvent.keyboard('Hello, World!'); - - await userEvent.click(within(dialog).getByRole('button', { name: 'Cancel' })); - - rerender(); - - expect(await dialogResult).toBe(null); - - expect(screen.queryByRole('dialog')).toBeFalsy(); - }); - }); - - describe('custom dialog', () => { - test('can show and hide', async () => { - function CustomDialog({ open, onClose }: DialogProps) { - return open ? ( -
- Hello -
- ) : null; - } - const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); - - result.current.open(CustomDialog); - - const dialog = await screen.findByRole('dialog'); - - rerender(); - - expect(within(dialog).getByText('Hello')).toBeTruthy(); - - await userEvent.click(within(dialog).getByRole('button', { name: 'Close me' })); - - rerender(); - - expect(screen.queryByRole('dialog')).toBeFalsy(); - }); - - test('can pass a payload', async () => { - function CustomDialog({ open, onClose, payload }: DialogProps) { - return open ? ( -
- {payload} -
- ) : null; - } - const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); - - result.current.open(CustomDialog, 'I am content'); - - const dialog = await screen.findByRole('dialog'); - - rerender(); - - expect(within(dialog).getByText('I am content')).toBeTruthy(); - }); - - test('can receive result', async () => { - function CustomDialog({ open, onClose }: DialogProps) { - return open ? ( -
- Hello -
- ) : null; - } - const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); - - const dialogResult = result.current.open(CustomDialog); - - const dialog = await screen.findByRole('dialog'); - - rerender(); - - await userEvent.click(within(dialog).getByRole('button', { name: 'Close me' })); - - rerender(); - - expect(await dialogResult).toBe('I am result'); - }); - - test('can close imperatively', async () => { - function CustomDialog({ open, onClose }: DialogProps) { - return open ? ( -
- Hello -
- ) : null; - } - const { result, rerender } = renderHook(() => useDialogs(), { wrapper: TestWrapper }); - - const theDialog = result.current.open(CustomDialog); - - const dialog = await screen.findByRole('dialog'); - - rerender(); - - expect(within(dialog).getByText('Hello')).toBeTruthy(); - - await result.current.close(theDialog, null); - - rerender(); - - expect(screen.queryByRole('dialog')).toBeFalsy(); - }); - }); -}); diff --git a/packages/toolpad-core/src/useDialogs/useDialogs.tsx b/packages/toolpad-core/src/useDialogs/useDialogs.tsx deleted file mode 100644 index a6661fbbe32..00000000000 --- a/packages/toolpad-core/src/useDialogs/useDialogs.tsx +++ /dev/null @@ -1,368 +0,0 @@ -'use client'; -import Button from '@mui/material/Button'; -import Dialog from '@mui/material/Dialog'; -import DialogTitle from '@mui/material/DialogTitle'; -import DialogContent from '@mui/material/DialogContent'; -import DialogActions from '@mui/material/DialogActions'; -import TextField from '@mui/material/TextField'; -import DialogContentText from '@mui/material/DialogContentText'; -import { useNonNullableContext } from '@toolpad/utils/react'; -import invariant from 'invariant'; -import * as React from 'react'; -import { DialogsContext } from './DialogsContext'; -import { WindowContext } from '../shared/context'; -import { useLocaleText, type LocaleText } from '../AppProvider/LocalizationProvider'; - -interface DialogsProviderLocaleText { - alert: string; - confirm: string; - cancel: string; - ok: string; -} - -const defaultLocaleText: Pick = { - alert: 'Alert', - confirm: 'Confirm', - cancel: 'Cancel', - ok: 'Ok', -}; - -export interface OpenDialogOptions { - /** - * A function that is called before closing the dialog closes. The dialog - * stays open as long as the returned promise is not resolved. Use this if - * you want to perform an async action on close and show a loading state. - * - * @param result The result that the dialog will return after closing. - * @returns A promise that resolves when the dialog can be closed. - */ - onClose?: (result: R) => Promise; -} - -export interface AlertOptions extends OpenDialogOptions { - /** - * A title for the dialog. Defaults to `'Alert'`. - */ - title?: React.ReactNode; - /** - * The text to show in the "Ok" button. Defaults to `'Ok'`. - */ - okText?: React.ReactNode; -} - -export interface ConfirmOptions extends OpenDialogOptions { - /** - * A title for the dialog. Defaults to `'Confirm'`. - */ - title?: React.ReactNode; - /** - * The text to show in the "Ok" button. Defaults to `'Ok'`. - */ - okText?: React.ReactNode; - /** - * Denotes the purpose of the dialog. This will affect the color of the - * "Ok" button. Defaults to `undefined`. - */ - severity?: 'error' | 'info' | 'success' | 'warning'; - /** - * The text to show in the "Cancel" button. Defaults to `'Cancel'`. - */ - cancelText?: React.ReactNode; -} - -export interface PromptOptions extends OpenDialogOptions { - /** - * A title for the dialog. Defaults to `'Prompt'`. - */ - title?: React.ReactNode; - /** - * The text to show in the "Ok" button. Defaults to `'Ok'`. - */ - okText?: React.ReactNode; - /** - * The text to show in the "Cancel" button. Defaults to `'Cancel'`. - */ - cancelText?: React.ReactNode; -} - -/** - * The props that are passed to a dialog component. - */ -export interface DialogProps

{ - /** - * The payload that was passed when the dialog was opened. - */ - payload: P; - /** - * Whether the dialog is open. - */ - open: boolean; - /** - * A function to call when the dialog should be closed. If the dialog has a return - * value, it should be passed as an argument to this function. You should use the promise - * that is returned to show a loading state while the dialog is performing async actions - * on close. - * @param result The result to return from the dialog. - * @returns A promise that resolves when the dialog can be fully closed. - */ - onClose: (result: R) => Promise; -} - -export interface OpenAlertDialog { - /** - * Open an alert dialog. Returns a promise that resolves when the user - * closes the dialog. - * - * @param msg The message to show in the dialog. - * @param options Additional options for the dialog. - * @returns A promise that resolves when the dialog is closed. - */ - (msg: React.ReactNode, options?: AlertOptions): Promise; -} - -export interface OpenConfirmDialog { - /** - * Open a confirmation dialog. Returns a promise that resolves to true if - * the user confirms, false if the user cancels. - * - * @param msg The message to show in the dialog. - * @param options Additional options for the dialog. - * @returns A promise that resolves to true if the user confirms, false if the user cancels. - */ - (msg: React.ReactNode, options?: ConfirmOptions): Promise; -} - -export interface OpenPromptDialog { - /** - * Open a prompt dialog to request user input. Returns a promise that resolves to the input - * if the user confirms, null if the user cancels. - * - * @param msg The message to show in the dialog. - * @param options Additional options for the dialog. - * @returns A promise that resolves to the user input if the user confirms, null if the user cancels. - */ - (msg: React.ReactNode, options?: PromptOptions): Promise; -} - -export type DialogComponent = React.ComponentType>; - -export interface OpenDialog { - /** - * Open a dialog without payload. - * @param Component The dialog component to open. - * @param options Additional options for the dialog. - */ -

( - Component: DialogComponent, - payload?: P, - options?: OpenDialogOptions, - ): Promise; - /** - * Open a dialog and pass a payload. - * @param Component The dialog component to open. - * @param payload The payload to pass to the dialog. - * @param options Additional options for the dialog. - */ - (Component: DialogComponent, payload: P, options?: OpenDialogOptions): Promise; -} - -export interface CloseDialog { - /** - * Close a dialog and return a result. - * @param dialog The dialog to close. The promise returned by `open`. - * @param result The result to return from the dialog. - * @returns A promise that resolves when the dialog is fully closed. - */ - (dialog: Promise, result: R): Promise; -} - -export interface DialogHook { - alert: OpenAlertDialog; - confirm: OpenConfirmDialog; - prompt: OpenPromptDialog; - open: OpenDialog; - close: CloseDialog; -} - -function useDialogLoadingButton(onClose: () => Promise) { - const [loading, setLoading] = React.useState(false); - const handleClick = async () => { - try { - setLoading(true); - await onClose(); - } finally { - setLoading(false); - } - }; - return { - onClick: handleClick, - loading, - }; -} - -export interface AlertDialogPayload extends AlertOptions { - msg: React.ReactNode; -} - -export interface AlertDialogProps extends DialogProps {} - -export function AlertDialog({ open, payload, onClose }: AlertDialogProps) { - const appWindowContext = React.useContext(WindowContext); - - const globalLocaleText = useLocaleText(); - const localeText = { ...defaultLocaleText, ...globalLocaleText }; - const okButtonProps = useDialogLoadingButton(() => onClose()); - return ( -

onClose()} - container={appWindowContext?.document.body} - > - {payload.title ?? localeText.alert} - {payload.msg} - - - - - ); -} - -export interface ConfirmDialogPayload extends ConfirmOptions { - msg: React.ReactNode; -} - -export interface ConfirmDialogProps extends DialogProps {} - -export function ConfirmDialog({ open, payload, onClose }: ConfirmDialogProps) { - const appWindowContext = React.useContext(WindowContext); - - const globalLocaleText = useLocaleText(); - const localeText = { ...defaultLocaleText, ...globalLocaleText }; - const cancelButtonProps = useDialogLoadingButton(() => onClose(false)); - const okButtonProps = useDialogLoadingButton(() => onClose(true)); - return ( - onClose(false)} - container={appWindowContext?.document.body} - > - {payload.title ?? localeText.confirm} - {payload.msg} - - - - - - ); -} - -export interface PromptDialogPayload extends PromptOptions { - msg: React.ReactNode; -} - -export interface PromptDialogProps extends DialogProps {} - -export function PromptDialog({ open, payload, onClose }: PromptDialogProps) { - const appWindowContext = React.useContext(WindowContext); - - const globalLocaleText = useLocaleText(); - const localeText = { ...defaultLocaleText, ...globalLocaleText }; - const [input, setInput] = React.useState(''); - const cancelButtonProps = useDialogLoadingButton(() => onClose(null)); - - const [loading, setLoading] = React.useState(false); - - const name = 'input'; - return ( - onClose(null)} - PaperProps={{ - component: 'form', - onSubmit: async (event: React.FormEvent) => { - event.preventDefault(); - try { - setLoading(true); - const formData = new FormData(event.currentTarget); - const value = formData.get(name) ?? ''; - invariant(typeof value === 'string', 'Value must come from a text input'); - await onClose(value); - } finally { - setLoading(false); - } - }, - }} - container={appWindowContext?.document.body} - > - {payload.title ?? localeText.confirm} - - {payload.msg} - setInput(event.target.value)} - /> - - - - - - - ); -} - -export function useDialogs(): DialogHook { - const { open, close } = useNonNullableContext(DialogsContext); - - const alert = React.useCallback( - async (msg, { onClose, ...options } = {}) => - open(AlertDialog, { ...options, msg }, { onClose }), - [open], - ); - - const confirm = React.useCallback( - async (msg, { onClose, ...options } = {}) => - open(ConfirmDialog, { ...options, msg }, { onClose }), - [open], - ); - - const prompt = React.useCallback( - async (msg, { onClose, ...options } = {}) => - open(PromptDialog, { ...options, msg }, { onClose }), - [open], - ); - - return React.useMemo( - () => ({ - alert, - confirm, - prompt, - open, - close, - }), - [alert, close, confirm, open, prompt], - ); -} diff --git a/packages/toolpad-core/src/useLocalStorageState/index.ts b/packages/toolpad-core/src/useLocalStorageState/index.ts deleted file mode 100644 index 7ee432aa1bd..00000000000 --- a/packages/toolpad-core/src/useLocalStorageState/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useLocalStorageState'; diff --git a/packages/toolpad-core/src/useLocalStorageState/useLocalStorageState.tsx b/packages/toolpad-core/src/useLocalStorageState/useLocalStorageState.tsx deleted file mode 100644 index ad3b35a5d57..00000000000 --- a/packages/toolpad-core/src/useLocalStorageState/useLocalStorageState.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client'; -import { UseStorageState, useStorageState, useStorageStateServer } from '../persistence'; - -/** - * Sync state to local storage so that it persists through a page refresh. Usage is - * similar to useState except we pass in a storage key so that we can default - * to that value on page load instead of the specified initial value. - * - * Since the storage API isn't available in server-rendering environments, we - * return null during SSR and hydration. - */ -const useLocalStorageStateBrowser: UseStorageState = (...args: [any, any, any]) => - useStorageState(window.localStorage, ...args); - -export const useLocalStorageState: UseStorageState = - typeof window === 'undefined' ? useStorageStateServer : useLocalStorageStateBrowser; diff --git a/packages/toolpad-core/src/useNotifications/NotificationsContext.ts b/packages/toolpad-core/src/useNotifications/NotificationsContext.ts deleted file mode 100644 index 330f4a97385..00000000000 --- a/packages/toolpad-core/src/useNotifications/NotificationsContext.ts +++ /dev/null @@ -1,14 +0,0 @@ -'use client'; -import * as React from 'react'; -import type { ShowNotification, CloseNotification } from './useNotifications'; - -/** - * @ignore - internal component. - */ - -export interface NotificationsContextValue { - show: ShowNotification; - close: CloseNotification; -} - -export const NotificationsContext = React.createContext(null); diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx b/packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx deleted file mode 100644 index 3aa3f618cad..00000000000 --- a/packages/toolpad-core/src/useNotifications/NotificationsProvider.test.tsx +++ /dev/null @@ -1,19 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test } from 'vitest'; -import describeConformance from '@toolpad/utils/describeConformance'; -import { NotificationsProvider } from './NotificationsProvider'; - -describe('NotificationsProvider', () => { - describeConformance(, () => ({ - skip: ['themeDefaultProps'], - slots: { - snackbar: {}, - }, - })); - - test('dummy test', () => {}); -}); diff --git a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx b/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx deleted file mode 100644 index 0c9554e0bec..00000000000 --- a/packages/toolpad-core/src/useNotifications/NotificationsProvider.tsx +++ /dev/null @@ -1,212 +0,0 @@ -'use client'; -import * as React from 'react'; -import { - Alert, - Badge, - Button, - CloseReason, - IconButton, - Snackbar, - SnackbarCloseReason, - SnackbarContent, - SnackbarProps, -} from '@mui/material'; -import CloseIcon from '@mui/icons-material/Close'; -import { useNonNullableContext } from '@toolpad/utils/react'; -import useSlotProps from '@mui/utils/useSlotProps'; -import { NotificationsContext } from './NotificationsContext'; -import type { - CloseNotification, - ShowNotification, - ShowNotificationOptions, -} from './useNotifications'; -import { useLocaleText, type LocaleText } from '../AppProvider/LocalizationProvider'; - -export interface NotificationsProviderSlotProps { - snackbar: SnackbarProps; -} - -export interface NotificationsProviderSlots { - /** - * The component that renders the snackbar. - * @default Snackbar - */ - snackbar: React.ElementType; -} - -const RootPropsContext = React.createContext(null); - -interface NotificationsProviderLocaleText { - close: string; -} - -const defaultLocaleText: Pick = { - close: 'Close', -}; - -interface NotificationProps { - notificationKey: string; - badge: string | null; - open: boolean; - message: React.ReactNode; - options: ShowNotificationOptions; -} - -function Notification({ notificationKey, open, message, options, badge }: NotificationProps) { - const globalLocaleText = useLocaleText(); - const localeText = { ...defaultLocaleText, ...globalLocaleText }; - const { close } = useNonNullableContext(NotificationsContext); - - const { severity, actionText, onAction, autoHideDuration } = options; - - const handleClose = React.useCallback( - (event: unknown, reason?: CloseReason | SnackbarCloseReason) => { - if (reason === 'clickaway') { - return; - } - close(notificationKey); - }, - [notificationKey, close], - ); - - const action = ( - - {onAction ? ( - - ) : null} - - - - - ); - - const props = React.useContext(RootPropsContext); - const SnackbarComponent = props?.slots?.snackbar ?? Snackbar; - const snackbarSlotProps = useSlotProps({ - elementType: SnackbarComponent, - ownerState: props, - externalSlotProps: props?.slotProps?.snackbar, - additionalProps: { - open, - autoHideDuration, - onClose: handleClose, - action, - }, - }); - - return ( - - - {severity ? ( - - {message} - - ) : ( - - )} - - - ); -} - -interface NotificationQueueEntry { - notificationKey: string; - options: ShowNotificationOptions; - open: boolean; - message: React.ReactNode; -} - -interface NotificationsState { - queue: NotificationQueueEntry[]; -} - -interface NotificationsProps { - state: NotificationsState; -} - -function Notifications({ state }: NotificationsProps) { - const currentNotification = state.queue[0] ?? null; - - return currentNotification ? ( - 1 ? String(state.queue.length) : null} - /> - ) : null; -} - -export interface NotificationsProviderProps { - children?: React.ReactNode; - // eslint-disable-next-line react/no-unused-prop-types - slots?: Partial; - // eslint-disable-next-line react/no-unused-prop-types - slotProps?: Partial; -} - -let nextId = 0; -const generateId = () => { - const id = nextId; - nextId += 1; - return id; -}; - -/** - * Provider for Notifications. The subtree of this component can use the `useNotifications` hook to - * access the notifications API. The notifications are shown in the same order they are requested. - * - * Demos: - * - * - [Sign-in Page](https://mui.com/toolpad/core/react-sign-in-page/) - * - [useNotifications](https://mui.com/toolpad/core/react-use-notifications/) - * - * API: - * - * - [NotificationsProvider API](https://mui.com/toolpad/core/api/notifications-provider) - */ -function NotificationsProvider(props: NotificationsProviderProps) { - const { children } = props; - const [state, setState] = React.useState({ queue: [] }); - - const show = React.useCallback((message, options = {}) => { - const notificationKey = options.key ?? `::toolpad-internal::notification::${generateId()}`; - setState((prev) => { - if (prev.queue.some((n) => n.notificationKey === notificationKey)) { - // deduplicate by key - return prev; - } - return { - ...prev, - queue: [...prev.queue, { message, options, notificationKey, open: true }], - }; - }); - return notificationKey; - }, []); - - const close = React.useCallback((key) => { - setState((prev) => ({ - ...prev, - queue: prev.queue.filter((n) => n.notificationKey !== key), - })); - }, []); - - const contextValue = React.useMemo(() => ({ show, close }), [show, close]); - - return ( - - - {children} - - - - ); -} - -export { NotificationsProvider }; diff --git a/packages/toolpad-core/src/useNotifications/index.ts b/packages/toolpad-core/src/useNotifications/index.ts deleted file mode 100644 index 38ae998b741..00000000000 --- a/packages/toolpad-core/src/useNotifications/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './useNotifications'; -export * from './NotificationsProvider'; diff --git a/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx b/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx deleted file mode 100644 index 9c3d719200d..00000000000 --- a/packages/toolpad-core/src/useNotifications/useNotifications.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { describe, test, expect } from 'vitest'; -import { renderHook, within, screen } from '@testing-library/react'; -import { userEvent } from '@testing-library/user-event'; -import { useNotifications } from './useNotifications'; -import { NotificationsProvider } from './NotificationsProvider'; - -interface TestWrapperProps { - children: React.ReactNode; -} - -function TestWrapper({ children }: TestWrapperProps) { - return {children}; -} - -describe('useNotifications', () => { - test('can do basic notifications', async () => { - const { result, rerender } = renderHook(() => useNotifications(), { wrapper: TestWrapper }); - - expect(screen.queryByRole('alert')).toBeNull(); - - const key = result.current.show('Hello'); - expect(key).toBeTypeOf('string'); - - rerender(); - - const snackbar = screen.getByRole('alert'); - expect(snackbar.textContent).toBe('Hello'); - - await userEvent.click(within(snackbar).getByRole('button', { name: 'Close' })); - - rerender(); - - expect(screen.queryByRole('alert')).toBeNull(); - }); -}); diff --git a/packages/toolpad-core/src/useNotifications/useNotifications.tsx b/packages/toolpad-core/src/useNotifications/useNotifications.tsx deleted file mode 100644 index 91dcb340aec..00000000000 --- a/packages/toolpad-core/src/useNotifications/useNotifications.tsx +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from 'react'; -import { NotificationsContext } from './NotificationsContext'; - -export interface ShowNotificationOptions { - /** - * The key to use for deduping notifications. If not provided, a unique key will be generated. - */ - key?: string; - /** - * The severity of the notification. When provided, the snackbar will show an alert with the - * specified severity. - */ - severity?: 'info' | 'warning' | 'error' | 'success'; - /** - * The duration in milliseconds after which the notification will automatically close. - */ - autoHideDuration?: number; - /** - * The text to display on the action button. - */ - actionText?: React.ReactNode; - /** - * The callback to call when the action button is clicked. - */ - onAction?: () => void; -} - -export interface ShowNotification { - /** - * Show a snackbar in the application. - * - * @param message The message to display in the snackbar. - * @param options Options for the snackbar. - * @returns The key that represents the notification. Useful for programmatically - * closing it. - */ - (message: React.ReactNode, options?: ShowNotificationOptions): string; -} - -export interface CloseNotification { - /** - * Close a snackbar in the application. - * - * @param key The key of the notification to close. - */ - (key: string): void; -} - -interface UseNotifications { - show: ShowNotification; - close: CloseNotification; -} - -const serverNotifications: UseNotifications = { - show: () => { - throw new Error('Not supported on server side'); - }, - close: () => { - throw new Error('Not supported on server side'); - }, -}; - -export function useNotifications(): UseNotifications { - const context = React.useContext(NotificationsContext); - - if (context) { - return context; - } - - return serverNotifications; -} diff --git a/packages/toolpad-core/src/useSearchParamState/index.ts b/packages/toolpad-core/src/useSearchParamState/index.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/packages/toolpad-core/src/useSession/index.ts b/packages/toolpad-core/src/useSession/index.ts deleted file mode 100644 index c47a6987dd9..00000000000 --- a/packages/toolpad-core/src/useSession/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './useSession'; diff --git a/packages/toolpad-core/src/useSession/useSession.test.tsx b/packages/toolpad-core/src/useSession/useSession.test.tsx deleted file mode 100644 index d0add057a91..00000000000 --- a/packages/toolpad-core/src/useSession/useSession.test.tsx +++ /dev/null @@ -1,45 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { renderHook } from '@testing-library/react'; -import { describe, test, expect } from 'vitest'; -import { useSession } from './useSession'; -import { Session, SessionContext } from '../AppProvider/AppProvider'; - -// Mock the session data -const mockSession = { - user: { - name: 'Bharat Kashyap', - email: 'bharat@mui.com', - image: 'https://avatars.githubusercontent.com/u/19550456', - }, -}; - -interface TestWrapperProps { - session: Session | null; - children: React.ReactNode; -} - -function TestWrapper({ children, session }: TestWrapperProps) { - return {children}; -} - -describe('useSession hook', () => { - test('should return session data when authenticated', () => { - const { result } = renderHook(() => useSession(), { - wrapper: ({ children }) => {children}, - }); - - expect(result.current).toEqual(mockSession); - }); - - test('should return null session when not authenticated', () => { - const { result } = renderHook(() => useSession(), { - wrapper: ({ children }) => {children}, - }); - - expect(result.current).toBeNull(); - }); -}); diff --git a/packages/toolpad-core/src/useSession/useSession.ts b/packages/toolpad-core/src/useSession/useSession.ts deleted file mode 100644 index 41742b233c6..00000000000 --- a/packages/toolpad-core/src/useSession/useSession.ts +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; -import { SessionContext, Session } from '../AppProvider'; - -/** - * Hook to access the current Toolpad Core session. - * @returns The current session object or null if no session is available. - */ -export function useSession(): T | null { - const session = React.useContext(SessionContext); - return session as T | null; -} diff --git a/packages/toolpad-core/src/useSessionStorageState/index.tsx b/packages/toolpad-core/src/useSessionStorageState/index.tsx deleted file mode 100644 index 0926909e726..00000000000 --- a/packages/toolpad-core/src/useSessionStorageState/index.tsx +++ /dev/null @@ -1 +0,0 @@ -export * from './useSessionStorageState'; diff --git a/packages/toolpad-core/src/useSessionStorageState/useSessionStorageState.tsx b/packages/toolpad-core/src/useSessionStorageState/useSessionStorageState.tsx deleted file mode 100644 index ff2db57ed2a..00000000000 --- a/packages/toolpad-core/src/useSessionStorageState/useSessionStorageState.tsx +++ /dev/null @@ -1,16 +0,0 @@ -'use client'; -import { UseStorageState, useStorageState, useStorageStateServer } from '../persistence'; - -/** - * Sync state to session storage so that it persists through a page refresh. Usage is - * similar to useState except we pass in a storage key so that we can default - * to that value on page load instead of the specified initial value. - * - * Since the storage API isn't available in server-rendering environments, we - * return null during SSR and hydration. - */ -const useSessionStorageStateBrowser: UseStorageState = (...args: [any, any, any]) => - useStorageState(window.sessionStorage, ...args); - -export const useSessionStorageState: UseStorageState = - typeof window === 'undefined' ? useStorageStateServer : useSessionStorageStateBrowser; diff --git a/packages/toolpad-core/tsconfig.build.json b/packages/toolpad-core/tsconfig.build.json deleted file mode 100644 index b900083b5ee..00000000000 --- a/packages/toolpad-core/tsconfig.build.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - // This config is for emitting declarations (.d.ts) only - // Actual .ts source files are transpiled via babel - "extends": "./tsconfig.json", - "compilerOptions": { - "declaration": true, - "noEmit": false, - "emitDeclarationOnly": true, - "outDir": "build" - }, - "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] -} diff --git a/packages/toolpad-core/tsconfig.json b/packages/toolpad-core/tsconfig.json deleted file mode 100644 index 9dc59d141cf..00000000000 --- a/packages/toolpad-core/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./build/tsconfig.json.tsbuildinfo", - "module": "esnext", - "target": "es2022", - "lib": ["es2022", "dom"], - "jsx": "preserve", - "moduleResolution": "bundler", - "forceConsistentCasingInFileNames": true, - "strict": true, - "skipLibCheck": true, - "noEmit": true, - "experimentalDecorators": true, - "allowSyntheticDefaultImports": true, - "noErrorTruncation": false, - "allowJs": true, - "outDir": "build", - "rootDir": "src" - }, - "include": ["src/**/*"], - "exclude": ["build", "node_modules"], - "references": [{ "path": "../toolpad-utils" }] -} diff --git a/packages/toolpad-core/vitest.config.mts b/packages/toolpad-core/vitest.config.mts deleted file mode 100644 index 649875ff736..00000000000 --- a/packages/toolpad-core/vitest.config.mts +++ /dev/null @@ -1,24 +0,0 @@ -import { defineConfig } from 'vitest/config'; -import react from '@vitejs/plugin-react'; - -export default defineConfig({ - plugins: [react()], - test: { - setupFiles: ['../../test/setupVitest.ts', '@testing-library/jest-dom/vitest'], - browser: { - enabled: false, // enabled through CLI - name: 'chromium', - provider: 'playwright', - headless: !!process.env.CI, - viewport: { - width: 1024, - height: 896, - }, - }, - coverage: { - exclude: ['./build/**'], - reportsDirectory: './.coverage', - reporter: ['text', 'lcov'], - }, - }, -}); diff --git a/packages/toolpad-studio-components/package.json b/packages/toolpad-studio-components/package.json index 9462cc2ae01..0a1fd3dc4b0 100644 --- a/packages/toolpad-studio-components/package.json +++ b/packages/toolpad-studio-components/package.json @@ -50,7 +50,7 @@ "@mui/x-license": "7.28.0", "@tanstack/react-query": "5.69.0", "@toolpad/studio-runtime": "workspace:*", - "@toolpad/utils": "workspace:*", + "@toolpad/utils": "0.13.0", "dayjs": "1.11.13", "invariant": "2.2.4", "markdown-to-jsx": "7.7.4", diff --git a/packages/toolpad-studio-runtime/package.json b/packages/toolpad-studio-runtime/package.json index 7a3813b9372..a0f9e7a9f89 100644 --- a/packages/toolpad-studio-runtime/package.json +++ b/packages/toolpad-studio-runtime/package.json @@ -46,7 +46,7 @@ "@auth/core": "0.38.0", "@mui/material": "6.4.7", "@tanstack/react-query": "5.69.0", - "@toolpad/utils": "workspace:*", + "@toolpad/utils": "0.13.0", "@types/json-schema": "7.0.15", "@webcontainer/env": "1.1.1", "cookie": "1.0.2", diff --git a/packages/toolpad-studio/package.json b/packages/toolpad-studio/package.json index 0c0b6adb3d6..c4562eb520d 100644 --- a/packages/toolpad-studio/package.json +++ b/packages/toolpad-studio/package.json @@ -78,10 +78,10 @@ "@mui/x-tree-view": "7.28.0", "@tanstack/react-query": "5.69.0", "@tanstack/react-query-devtools": "5.69.0", - "@toolpad/core": "workspace:*", + "@toolpad/core": "0.13.0", "@toolpad/studio-components": "workspace:*", "@toolpad/studio-runtime": "workspace:*", - "@toolpad/utils": "workspace:*", + "@toolpad/utils": "0.13.0", "@types/cors": "2.8.17", "@types/json-schema": "7.0.15", "@types/node": "^20.17.24", diff --git a/packages/toolpad-utils/README.md b/packages/toolpad-utils/README.md deleted file mode 100644 index a22ee1a9acf..00000000000 --- a/packages/toolpad-utils/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# @toolpad/utils - -Shared utilities used by Toolpad packages. diff --git a/packages/toolpad-utils/package.json b/packages/toolpad-utils/package.json deleted file mode 100644 index 3c3fea0d0f1..00000000000 --- a/packages/toolpad-utils/package.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "name": "@toolpad/utils", - "version": "0.13.0", - "description": "Shared utilities used by Toolpad packages.", - "author": "MUI Toolpad team", - "homepage": "https://github.com/mui/toolpad#readme", - "license": "MIT", - "main": "./build/index.cjs", - "module": "./build/index.js", - "types": "./build/index.d.ts", - "exports": { - "./package.json": "./package.json", - "./*": "./src/*.ts", - "./hooks/*": "./src/hooks/*.ts" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/mui/toolpad.git", - "directory": "packages/toolpad-studio-utils" - }, - "scripts": { - "clean": "rimraf build", - "prebuild": "pnpm clean", - "build": "pnpm build:node && pnpm build:stable && pnpm build:modern && pnpm build:types && pnpm build:copy-files", - "build:node": "node ../../scripts/build.mjs node --use-exports", - "build:modern": "node ../../scripts/build.mjs modern --use-exports", - "build:stable": "node ../../scripts/build.mjs stable --use-exports", - "build:copy-files": "node ../../scripts/copyFiles.mjs --use-exports", - "build:types": "tsx ../../scripts/buildTypes.mts", - "predev": "pnpm clean", - "dev": "mkdir -p build && concurrently \"pnpm build:stable --watch\" \"pnpm build:types --watch\" \"pnpm build:copy-files\"", - "check-types": "pnpm build:stable && pnpm build:types && tsc --noEmit", - "test": "vitest run --coverage" - }, - "bugs": { - "url": "https://github.com/mui/toolpad/issues" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - }, - "dependencies": { - "invariant": "2.2.4", - "prettier": "3.4.2", - "react-is": "^19.0.0", - "title": "4.0.1", - "yaml": "2.5.1", - "yaml-diff-patch": "2.0.0" - }, - "devDependencies": { - "@types/express": "5.0.0", - "@types/invariant": "2.2.37", - "@types/react": "^19.0.10", - "@types/react-is": "^19.0.0", - "@types/title": "3.4.3", - "vitest": "2.1.9" - }, - "publishConfig": { - "access": "public", - "directory": "build" - } -} diff --git a/packages/toolpad-utils/src/cli.spec.ts b/packages/toolpad-utils/src/cli.spec.ts deleted file mode 100644 index c7598452673..00000000000 --- a/packages/toolpad-utils/src/cli.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { test, expect } from 'vitest'; -import path from 'path'; -import os from 'os'; -import { bashResolvePath } from './cli'; - -test('bashResolvePath', async () => { - const homeAbsoluteUrl = bashResolvePath('~/test'); - const cwdAbsoluteUrl = bashResolvePath('./test'); - expect(homeAbsoluteUrl).toEqual(path.resolve(os.homedir(), 'test')); - expect(cwdAbsoluteUrl).toEqual(path.resolve(process.cwd(), './test')); -}); diff --git a/packages/toolpad-utils/src/cli.ts b/packages/toolpad-utils/src/cli.ts deleted file mode 100644 index 4517d512cfb..00000000000 --- a/packages/toolpad-utils/src/cli.ts +++ /dev/null @@ -1,9 +0,0 @@ -import os from 'os'; -import path from 'path'; - -// https://www.gnu.org/software/bash/manual/html_node/Tilde-Expansion.html -export function bashResolvePath(pathName: string) { - return pathName.startsWith('~/') - ? path.resolve(os.homedir(), pathName.slice(2)) - : path.resolve(process.cwd(), pathName); -} diff --git a/packages/toolpad-utils/src/collections.spec.ts b/packages/toolpad-utils/src/collections.spec.ts deleted file mode 100644 index 7ee9da03e7a..00000000000 --- a/packages/toolpad-utils/src/collections.spec.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { asArray, equalProperties } from './collections'; - -describe('asArray', () => { - test.each([ - ['hello', ['hello']], - [['hello'], ['hello']], - [undefined, [undefined]], - ['', ['']], - ])('should convert %p to %p', (got, expected) => { - expect(asArray(got)).toEqual(expected); - }); -}); - -const OBJECT = {}; - -describe('equalProperties', () => { - test.each([ - [{ a: 1, b: 2 }, { a: 1, b: 2 }, true], - [{ a: 1 }, { b: 1 }, false], - [{ a: {} }, { a: {} }, false], - [{ a: OBJECT }, { a: OBJECT }, true], - ])('should compare %p and %p', (obj1, obj2, expected) => { - expect(equalProperties(obj1, obj2)).toBe(expected); - }); -}); diff --git a/packages/toolpad-utils/src/collections.ts b/packages/toolpad-utils/src/collections.ts deleted file mode 100644 index 76699c8d41b..00000000000 --- a/packages/toolpad-utils/src/collections.ts +++ /dev/null @@ -1,107 +0,0 @@ -export function asArray(maybeArray: T | T[]): T[] { - return Array.isArray(maybeArray) ? maybeArray : [maybeArray]; -} - -type PropertiesOf

= Extract; - -type Require = T & { [P in K]-?: T[P] }; - -type Ensure = K extends keyof U ? Require : U & Record; - -/** - * Type aware version of Object.protoype.hasOwnProperty. - * See https://fettblog.eu/typescript-hasownproperty/ - */ -export function hasOwnProperty( - obj: X, - prop: Y, -): obj is Ensure { - return obj.hasOwnProperty(prop); -} - -/** - * Maps `obj` to a new object. The `mapper` function receives an entry array of key and value and - * is allowed to manipulate both. It can also return `null` to omit a property from the result. - */ -export function mapProperties( - obj: P, - mapper: >(old: [K, P[K]]) => [L, U] | null, -): Record; -export function mapProperties( - obj: Record, - mapper: (old: [string, U]) => [string, V] | null, -): Record { - return Object.fromEntries( - Object.entries(obj).flatMap((entry) => { - const mapped = mapper(entry); - return mapped ? [mapped] : []; - }), - ); -} - -/** - * Maps an objects' property keys. The result is a new object with the mapped keys, but the same values. - */ -export function mapKeys( - obj: Record, - mapper: (old: string) => string, -): Record { - return mapProperties(obj, ([key, value]) => [mapper(key), value]); -} - -/** - * Maps an objects' property values. The result is a new object with the same keys, but mapped values. - */ -export function mapValues( - obj: P, - mapper: (old: P[PropertiesOf

], key: PropertiesOf

) => V, -): Record, V> { - return mapProperties(obj, ([key, value]) => [key, mapper(value, key)]); -} -/** - * Filters an objects' property values. Similar to `array.filter` but for objects. The result is a new - * object with all the properties removed for which `filter` returned `false`. - */ -export function filterValues( - obj: Record, - filter: (old: P) => old is Q, -): Record; -export function filterValues

(obj: P, filter: (old: P[keyof P]) => boolean): Partial

; -export function filterValues( - obj: Record, - filter: (old: U) => boolean, -): Record; -export function filterValues( - obj: Record, - filter: (old: U) => boolean, -): Record { - return mapProperties(obj, ([key, value]) => (filter(value) ? [key, value] : null)); -} - -/** - * Filters an objects' property keys. Similar to `array.filter` but for objects. The result is a new - * object with all the properties removed for which `filter` returned `false`. - */ -export function filterKeys

(obj: P, filter: (old: keyof P) => boolean): Partial

; -export function filterKeys( - obj: Record, - filter: (old: string) => boolean, -): Record; -export function filterKeys( - obj: Record, - filter: (old: string) => boolean, -): Record { - return mapProperties(obj, ([key, value]) => (filter(key) ? [key, value] : null)); -} - -/** - * Compares the properties of two objects. Returns `true` if all properties are strictly equal, `false` - * otherwise. - * Pass a subset of properties to only compare those. - */ -export function equalProperties

(obj1: P, obj2: P, subset?: (keyof P)[]): boolean { - const keysToCheck = new Set( - subset ?? ([...Object.keys(obj1), ...Object.keys(obj2)] as (keyof P)[]), - ); - return Array.from(keysToCheck).every((key) => Object.is(obj1[key], obj2[key])); -} diff --git a/packages/toolpad-utils/src/comparators.spec.ts b/packages/toolpad-utils/src/comparators.spec.ts deleted file mode 100644 index 68d6f892807..00000000000 --- a/packages/toolpad-utils/src/comparators.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { - Comparator, - alphabeticComparator, - createPropComparator, - defaultComparator, -} from './comparators'; - -describe('createPropComparator', () => { - test.each([ - [ - [ - { a: 'asbestos', b: 2, c: 7 }, - { a: 'bureaucracy', b: 1, c: 8 }, - { a: 'charcoal', b: 4, c: 2 }, - ], - 'b', - defaultComparator, - [ - { a: 'bureaucracy', b: 1, c: 8 }, - { a: 'asbestos', b: 2, c: 7 }, - { a: 'charcoal', b: 4, c: 2 }, - ], - ], - [ - [ - { a: 'charcoal', b: 4, c: 2 }, - { a: 'asbestos', b: 2, c: 7 }, - { a: 'bureaucracy', b: 1, c: 8 }, - ], - 'c', - defaultComparator, - [ - { a: 'charcoal', b: 4, c: 2 }, - { a: 'asbestos', b: 2, c: 7 }, - { a: 'bureaucracy', b: 1, c: 8 }, - ], - ], - [ - [ - { a: 'asbestos', b: 2, c: 7 }, - { a: 'charcoal', b: 4, c: 2 }, - { a: 'Bureaucracy', b: 1, c: 8 }, - ], - 'a', - alphabeticComparator, - [ - { a: 'asbestos', b: 2, c: 7 }, - { a: 'Bureaucracy', b: 1, c: 8 }, - { a: 'charcoal', b: 4, c: 2 }, - ], - ], - ])('should sort %p to %p', (objs, prop, comparator, expected) => { - expect( - [...objs].sort( - createPropComparator(prop as 'a' | 'b' | 'c', comparator as Comparator), - ), - ).toEqual(expected); - }); -}); diff --git a/packages/toolpad-utils/src/comparators.ts b/packages/toolpad-utils/src/comparators.ts deleted file mode 100644 index 7ef0b4e0296..00000000000 --- a/packages/toolpad-utils/src/comparators.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface Comparator { - (a: T, b: T): number; -} - -export function defaultComparator(a: T, b: T): number { - if (a < b) { - return -1; - } - if (a > b) { - return 1; - } - return 0; -} - -export function alphabeticComparator(a: string, b: string): number { - const { compare } = new Intl.Collator(); - return compare(a, b); -} - -export function createPropComparator( - propName: K, - comparator: Comparator = defaultComparator, -): Comparator { - return (a, b) => comparator(a[propName], b[propName]); -} diff --git a/packages/toolpad-utils/src/describeConformance.ts b/packages/toolpad-utils/src/describeConformance.ts deleted file mode 100644 index 3dcaf7197fb..00000000000 --- a/packages/toolpad-utils/src/describeConformance.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; - -export interface ConformanceOptions { - refInstanceof?: unknown; - inheritComponent?: React.ElementType; - skip?: Array<'themeDefaultProps'>; -} - -export default function describeConformance( - minimalElement: React.ReactElement, - getOptions: () => ConformanceOptions, -) { - getOptions(); -} diff --git a/packages/toolpad-utils/src/errors.spec.ts b/packages/toolpad-utils/src/errors.spec.ts deleted file mode 100644 index c9afbd71b10..00000000000 --- a/packages/toolpad-utils/src/errors.spec.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { test, expect } from 'vitest'; -import { errorFrom } from './errors'; - -test('leaves errors untouched', () => { - const error = new Error('BOOM!'); - expect(errorFrom(error)).toBe(error); -}); - -test('converts objects', () => { - const error = { message: 'BOOM!' }; - const result = errorFrom(error); - expect(result).toBeInstanceOf(Error); - expect(result).toHaveProperty('message', 'BOOM!'); - expect(result).toHaveProperty('cause', error); -}); - -test('converts strings', () => { - const result = errorFrom('BOOM!'); - expect(result).toBeInstanceOf(Error); - expect(result).toHaveProperty('message', 'BOOM!'); - expect(result).toHaveProperty('cause', 'BOOM!'); -}); diff --git a/packages/toolpad-utils/src/errors.ts b/packages/toolpad-utils/src/errors.ts deleted file mode 100644 index 22740f8c27f..00000000000 --- a/packages/toolpad-utils/src/errors.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { hasOwnProperty } from './collections'; -import { truncate } from './strings'; - -declare global { - interface Error { - code?: unknown; - } -} - -export type PlainObject = Record; - -export interface SerializedError extends PlainObject { - message: string; - name: string; - stack?: string; - code?: unknown; -} - -export function serializeError(error: Error): SerializedError { - const { message, name, stack, code } = error; - return { message, name, stack, code }; -} - -/** - * Creates a javascript `Error` from an unknown value if it's not already an error. - * Does a best effort at inferring a message. Intended to be used typically in `catch` - * blocks, as there is no way to enforce only `Error` objects being thrown. - * - * ``` - * try { - * // ... - * } catch (rawError) { - * const error = errorFrom(rawError); - * console.assert(error instanceof Error); - * } - * ``` - */ -export function errorFrom(maybeError: unknown): Error { - if (maybeError instanceof Error) { - return maybeError; - } - - if ( - typeof maybeError === 'object' && - maybeError && - hasOwnProperty(maybeError, 'message') && - typeof maybeError.message === 'string' - ) { - return new Error(maybeError.message, { cause: maybeError }); - } - - if (typeof maybeError === 'string') { - return new Error(maybeError, { cause: maybeError }); - } - - const message = truncate(String(JSON.stringify(maybeError)), 500); - return new Error(message, { cause: maybeError }); -} diff --git a/packages/toolpad-utils/src/events.spec.ts b/packages/toolpad-utils/src/events.spec.ts deleted file mode 100644 index f5e1d815ac2..00000000000 --- a/packages/toolpad-utils/src/events.spec.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { vitest, test, expect } from 'vitest'; -import { Emitter } from './events'; - -test('can add/remove event handlers', async () => { - const handler = vitest.fn(); - const emitter = new Emitter<{ hello: number }>(); - emitter.on('hello', handler); - emitter.emit('hello', 1); - expect(handler).toBeCalledWith(1); - emitter.off('hello', handler); - emitter.emit('hello', 2); - expect(handler).not.toBeCalledWith(2); -}); - -test('can subscribe event handlers', async () => { - const handler = vitest.fn(); - const emitter = new Emitter<{ hello: number }>(); - const unsubscribe = emitter.subscribe('hello', handler); - emitter.emit('hello', 1); - expect(handler).toBeCalledWith(1); - unsubscribe(); - emitter.emit('hello', 2); - expect(handler).not.toBeCalledWith(2); -}); - -test('can add/remove event handlers for symbols', async () => { - const SYMBOL = Symbol('event'); - const handler = vitest.fn(); - const emitter = new Emitter<{ [SYMBOL]: number }>(); - emitter.on(SYMBOL, handler); - emitter.emit(SYMBOL, 1); - expect(handler).toBeCalledWith(1); - emitter.off(SYMBOL, handler); - emitter.emit(SYMBOL, 2); - expect(handler).not.toBeCalledWith(2); -}); - -test('only fires selected event', async () => { - const handler1 = vitest.fn(); - const handler2 = vitest.fn(); - const emitter = new Emitter<{ hello: number; world: number }>(); - emitter.on('hello', handler1); - emitter.on('world', handler2); - emitter.emit('hello', 1); - expect(handler1).toBeCalledWith(1); - expect(handler2).not.toBeCalledWith(1); -}); diff --git a/packages/toolpad-utils/src/events.ts b/packages/toolpad-utils/src/events.ts deleted file mode 100644 index a12d49dc815..00000000000 --- a/packages/toolpad-utils/src/events.ts +++ /dev/null @@ -1,76 +0,0 @@ -export type EventName = string | symbol; - -export type EventHandlers = Record; - -export type EventHandler = ( - event: T[K], -) => void; - -export type AllEventsHandler = ( - type: K, - event: T[K], -) => void; - -/** - * Lightweight event emitter - */ -export class Emitter { - private handlers = new Map | AllEventsHandler>>(); - - /** - * Add a listener for an event - */ - on(name: '*', handler: AllEventsHandler): void; - - on(name: K, handler: EventHandler): void; - - on(name: K | '*', handler: EventHandler | AllEventsHandler): void { - let eventHandlers = this.handlers.get(name); - if (!eventHandlers) { - eventHandlers = new Set(); - this.handlers.set(name, eventHandlers); - } - eventHandlers.add(handler as EventHandler | AllEventsHandler); - } - - /** - * Remove a listener from an event - */ - off(name: K, handler: EventHandler) { - const eventHandlers = this.handlers.get(name); - if (eventHandlers) { - eventHandlers.delete(handler as EventHandler | AllEventsHandler); - if (eventHandlers.size <= 0) { - this.handlers.delete(name); - } - } - } - - /** - * Subscribe to an event and return an unsubscribe function. - */ - subscribe(name: K, handler: EventHandler) { - this.on(name, handler); - return () => { - this.off(name, handler); - }; - } - - /** - * Emit an event. - */ - emit(name: K, event: T[K]) { - const eventHandlers = this.handlers.get(name); - if (eventHandlers) { - for (const eventHandler of eventHandlers) { - (eventHandler as EventHandler)(event); - } - } - const allHandlers = this.handlers.get('*'); - if (allHandlers) { - for (const eventHandler of allHandlers) { - (eventHandler as AllEventsHandler)(name, event); - } - } - } -} diff --git a/packages/toolpad-utils/src/fs.spec.ts b/packages/toolpad-utils/src/fs.spec.ts deleted file mode 100644 index edf88480a79..00000000000 --- a/packages/toolpad-utils/src/fs.spec.ts +++ /dev/null @@ -1,60 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import { describe, test, beforeEach, afterEach, expect } from 'vitest'; -import { fileReplace, fileReplaceAll } from './fs'; - -describe('fileReplace', () => { - let testDir: string; - - beforeEach(async () => { - testDir = await fs.mkdtemp('test-dir'); - }); - - afterEach(async () => { - await fs.rm(testDir, { recursive: true, force: true }); - }); - - test('can replace parts of a file', async () => { - const filePath = path.resolve(testDir, './test.txt'); - await fs.writeFile(filePath, 'Hello World, Hello', { encoding: 'utf-8' }); - await fileReplace(filePath, 'Hello', 'Goodbye'); - const content = await fs.readFile(filePath, { encoding: 'utf-8' }); - expect(content).toEqual('Goodbye World, Hello'); - }); - - test('can replace with double dollar signs', async () => { - const filePath = path.resolve(testDir, './test.txt'); - await fs.writeFile(filePath, 'Hello World', { encoding: 'utf-8' }); - await fileReplace(filePath, 'Hello', '$$'); - const content = await fs.readFile(filePath, { encoding: 'utf-8' }); - expect(content).toEqual('$$ World'); - }); -}); - -describe('fileReplaceAll', () => { - let testDir: string; - - beforeEach(async () => { - testDir = await fs.mkdtemp('test-dir'); - }); - - afterEach(async () => { - await fs.rm(testDir, { recursive: true, force: true }); - }); - - test('can replace parts of a file', async () => { - const filePath = path.resolve(testDir, './test.txt'); - await fs.writeFile(filePath, 'Hello World, Hello', { encoding: 'utf-8' }); - await fileReplaceAll(filePath, 'Hello', 'Goodbye'); - const content = await fs.readFile(filePath, { encoding: 'utf-8' }); - expect(content).toEqual('Goodbye World, Goodbye'); - }); - - test('can replace with double dollar signs', async () => { - const filePath = path.resolve(testDir, './test.txt'); - await fs.writeFile(filePath, 'Hello World', { encoding: 'utf-8' }); - await fileReplaceAll(filePath, 'Hello', '$$'); - const content = await fs.readFile(filePath, { encoding: 'utf-8' }); - expect(content).toEqual('$$ World'); - }); -}); diff --git a/packages/toolpad-utils/src/fs.ts b/packages/toolpad-utils/src/fs.ts deleted file mode 100644 index 840bbc5c35c..00000000000 --- a/packages/toolpad-utils/src/fs.ts +++ /dev/null @@ -1,132 +0,0 @@ -import * as fs from 'fs/promises'; -import * as path from 'path'; -import { Dirent } from 'fs'; -import * as yaml from 'yaml'; -import { yamlOverwrite } from 'yaml-diff-patch'; -import prettier from 'prettier'; -import { errorFrom } from './errors'; - -/** - * Formats a yaml source with `prettier`. - */ -async function formatYaml(code: string, filePath: string): Promise { - const readConfig = await prettier.resolveConfig(filePath); - return prettier.format(code, { - ...readConfig, - parser: 'yaml', - }); -} - -export type Reviver = NonNullable[1]>; - -/** - * Like `fs.readFile`, but for JSON files specifically. Will throw on malformed JSON. - */ -export async function readJsonFile(filePath: string, reviver?: Reviver): Promise { - const content = await fs.readFile(filePath, { encoding: 'utf-8' }); - return JSON.parse(content, reviver); -} - -export async function readMaybeFile(filePath: string): Promise { - try { - return await fs.readFile(filePath, { encoding: 'utf-8' }); - } catch (rawError) { - const error = errorFrom(rawError); - if (error.code === 'ENOENT' || error.code === 'EISDIR') { - return null; - } - throw error; - } -} - -export async function readMaybeDir(dirPath: string): Promise { - try { - return await fs.readdir(dirPath, { withFileTypes: true }); - } catch (rawError: unknown) { - const error = errorFrom(rawError); - if (error.code === 'ENOENT' || error.code === 'ENOTDIR') { - return []; - } - throw error; - } -} - -export type WriteFileOptions = Parameters[2]; - -export async function writeFileRecursive( - filePath: string, - content: string | Buffer, - options?: WriteFileOptions, -): Promise { - await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile(filePath, content, options); -} - -export interface UpdateYamlOptions { - schemaUrl?: string; -} - -export async function updateYamlFile( - filePath: string, - content: object, - options?: UpdateYamlOptions, -) { - const oldContent = await readMaybeFile(filePath); - - let newContent = oldContent ? yamlOverwrite(oldContent, content) : yaml.stringify(content); - - if (options?.schemaUrl) { - const yamlDoc = yaml.parseDocument(newContent); - yamlDoc.commentBefore = ` yaml-language-server: $schema=${options.schemaUrl}`; - newContent = yamlDoc.toString(); - } - - newContent = await formatYaml(newContent, filePath); - if (newContent !== oldContent) { - await writeFileRecursive(filePath, newContent); - } -} - -export async function fileExists(filepath: string): Promise { - try { - const stat = await fs.stat(filepath); - return stat.isFile(); - } catch (err) { - if (errorFrom(err).code === 'ENOENT') { - return false; - } - throw err; - } -} - -export async function folderExists(folderpath: string): Promise { - try { - const stat = await fs.stat(folderpath); - return stat.isDirectory(); - } catch (err) { - if (errorFrom(err).code === 'ENOENT') { - return false; - } - throw err; - } -} - -export async function fileReplace( - filePath: string, - searchValue: string | RegExp, - replaceValue: string, -): Promise { - const queriesFileContent = await fs.readFile(filePath, { encoding: 'utf-8' }); - const updatedFileContent = queriesFileContent.replace(searchValue, () => replaceValue); - await fs.writeFile(filePath, updatedFileContent); -} - -export async function fileReplaceAll( - filePath: string, - searchValue: string | RegExp, - replaceValue: string, -) { - const queriesFileContent = await fs.readFile(filePath, { encoding: 'utf-8' }); - const updatedFileContent = queriesFileContent.replaceAll(searchValue, () => replaceValue); - await fs.writeFile(filePath, updatedFileContent); -} diff --git a/packages/toolpad-utils/src/hooks/index.ts b/packages/toolpad-utils/src/hooks/index.ts deleted file mode 100644 index e847138d45f..00000000000 --- a/packages/toolpad-utils/src/hooks/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export { default as useBoolean } from './useBoolean'; -export { default as usePageTitle } from './usePageTitle'; diff --git a/packages/toolpad-utils/src/hooks/useBoolean.ts b/packages/toolpad-utils/src/hooks/useBoolean.ts deleted file mode 100644 index 8f02e9b2f8f..00000000000 --- a/packages/toolpad-utils/src/hooks/useBoolean.ts +++ /dev/null @@ -1,12 +0,0 @@ -import * as React from 'react'; - -/** - * A utility with shortcuts to manipulate boolean values. - */ -export default function useBoolean(initialValue: boolean) { - const [value, setValue] = React.useState(initialValue); - const toggle = React.useCallback(() => setValue((existing) => !existing), []); - const setTrue = React.useCallback(() => setValue(true), []); - const setFalse = React.useCallback(() => setValue(false), []); - return { value, setValue, toggle, setTrue, setFalse }; -} diff --git a/packages/toolpad-utils/src/hooks/useDebounced.ts b/packages/toolpad-utils/src/hooks/useDebounced.ts deleted file mode 100644 index 5ecfd4cd1c1..00000000000 --- a/packages/toolpad-utils/src/hooks/useDebounced.ts +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from 'react'; - -/** - * This hook allows you to debounce any fast changing value. The debounced value will only - * reflect the latest value when the useDebounce hook has not been called for the specified - * time period. - * - * Inspired by https://usehooks.com/useDebounce/ - */ -export default function useDebounced(value: T, delay: number): T { - const [debouncedValue, setDebouncedValue] = React.useState(() => value); - const timeoutIdRef = React.useRef(null); - - React.useEffect( - () => () => { - if (timeoutIdRef.current) { - clearTimeout(timeoutIdRef.current); - timeoutIdRef.current = null; - } - }, - [], - ); - - React.useEffect(() => { - timeoutIdRef.current = setTimeout(() => setDebouncedValue(() => value), delay); - - return () => { - if (timeoutIdRef.current) { - clearTimeout(timeoutIdRef.current); - timeoutIdRef.current = null; - } - }; - }, [value, delay]); - - return debouncedValue; -} diff --git a/packages/toolpad-utils/src/hooks/useDebouncedHandler.ts b/packages/toolpad-utils/src/hooks/useDebouncedHandler.ts deleted file mode 100644 index dbbedcde17e..00000000000 --- a/packages/toolpad-utils/src/hooks/useDebouncedHandler.ts +++ /dev/null @@ -1,71 +0,0 @@ -import * as React from 'react'; - -interface Handler

{ - (...params: P): void; -} - -interface DelayedInvocation

{ - startTime: number; - timeout: NodeJS.Timeout; - params: P; -} - -function defer

( - fn: React.MutableRefObject>, - params: P, - delay: number, -) { - const timeout = setTimeout(() => { - fn.current(...params); - }, delay); - - return { startTime: Date.now(), timeout, params }; -} - -/** - * Creates a debounced version of the handler that is passed. The invocation of [fn] is - * delayed for [delay] milliseconds from the last invocation of the debounced function. - * - * This implementation adds on the lodash implementation in that it handles updates to the - * delay value. - */ -export default function useDebouncedHandler

( - fn: Handler

, - delay: number, -): Handler

{ - const fnRef = React.useRef(fn); - React.useEffect(() => { - fnRef.current = fn; - }, [fn]); - - const delayedInvocation = React.useRef | null>(null); - - const clearCurrent = React.useCallback(() => { - if (delayedInvocation.current) { - clearTimeout(delayedInvocation.current.timeout); - delayedInvocation.current = null; - } - }, []); - - React.useEffect(() => { - if (!delayedInvocation.current) { - return; - } - - const { startTime, params } = delayedInvocation.current; - - const elapsed = Date.now() - startTime; - const newDelay = Math.max(delay - elapsed, 0); - - clearCurrent(); - delayedInvocation.current = defer(fnRef, params, newDelay); - }, [delay, clearCurrent]); - - return React.useCallback( - (...params: P) => { - clearCurrent(); - delayedInvocation.current = defer(fnRef, params, delay); - }, - [delay, clearCurrent], - ); -} diff --git a/packages/toolpad-utils/src/hooks/useLatest.ts b/packages/toolpad-utils/src/hooks/useLatest.ts deleted file mode 100644 index 2c29357b97b..00000000000 --- a/packages/toolpad-utils/src/hooks/useLatest.ts +++ /dev/null @@ -1,16 +0,0 @@ -import * as React from 'react'; - -/** - * Returns the latest non-null, non-undefined value that has been passed to it. - */ -function useLatest(value: T): T; -function useLatest(value: T | null | undefined): T | null | undefined; -function useLatest(value: T | null | undefined): T | null | undefined { - const [latest, setLatest] = React.useState(value); - if (latest !== value && value !== null && value !== undefined) { - setLatest(value); - } - return value ?? latest; -} - -export default useLatest; diff --git a/packages/toolpad-utils/src/hooks/usePageTitle.ts b/packages/toolpad-utils/src/hooks/usePageTitle.ts deleted file mode 100644 index e0dfd74c738..00000000000 --- a/packages/toolpad-utils/src/hooks/usePageTitle.ts +++ /dev/null @@ -1,14 +0,0 @@ -import * as React from 'react'; - -/** - * Sets the current document title. - */ -export default function usePageTitle(title: string) { - React.useEffect(() => { - const original = document.title; - document.title = title; - return () => { - document.title = original; - }; - }, [title]); -} diff --git a/packages/toolpad-utils/src/hooks/useSsr.ts b/packages/toolpad-utils/src/hooks/useSsr.ts deleted file mode 100644 index b22f810c9d4..00000000000 --- a/packages/toolpad-utils/src/hooks/useSsr.ts +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; - -function subscribe() { - return () => {}; -} - -function getSnapshot() { - return false; -} - -function getServerSnapshot() { - return true; -} - -/** - * Returns true when serverside rendering, or when hydrating. - */ -export default function useSsr() { - return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot); -} diff --git a/packages/toolpad-utils/src/hooks/useStorageState.ts b/packages/toolpad-utils/src/hooks/useStorageState.ts deleted file mode 100644 index 2913da2ba35..00000000000 --- a/packages/toolpad-utils/src/hooks/useStorageState.ts +++ /dev/null @@ -1,114 +0,0 @@ -import * as React from 'react'; -import { Emitter } from '../events'; - -// storage events only work across windows, we'll use an event emitter to announce within the window -const emitter = new Emitter>(); - -function subscribe(area: Storage, key: string, cb: () => void): () => void { - const storageHandler = (event: StorageEvent) => { - if (event.storageArea === area && event.key === key) { - cb(); - } - }; - window.addEventListener('storage', storageHandler); - emitter.on(key, cb); - return () => { - window.removeEventListener('storage', storageHandler); - emitter.off(key, cb); - }; -} - -function getSnapshot(area: Storage, key: string): string | null { - return area.getItem(key); -} - -function setValue(area: Storage, key: string, value: string | null) { - if (typeof window !== 'undefined') { - if (value === null) { - area.removeItem(key); - } else { - area.setItem(key, String(value)); - } - emitter.emit(key, null); - } -} - -type Initializer = () => T; - -type UseStorageStateHookResult = [T, React.Dispatch>]; - -function useStorageStateServer( - kind: 'session' | 'local', - key: string, - initializer: string | Initializer, -): UseStorageStateHookResult; -function useStorageStateServer( - kind: 'session' | 'local', - key: string, - initializer?: string | null | Initializer, -): UseStorageStateHookResult; -function useStorageStateServer( - kind: 'session' | 'local', - key: string, - initializer: string | null | Initializer = null, -): UseStorageStateHookResult | UseStorageStateHookResult { - const [initialValue] = React.useState(initializer); - return [initialValue, () => {}]; -} - -/** - * Sync state to local/session storage so that it persists through a page refresh. Usage is - * similar to useState except we pass in a storage key so that we can default - * to that value on page load instead of the specified initial value. - * - * Since the storage API isn't available in server-rendering environments, we - * return initialValue during SSR and hydration. - * - * Things this hook does different from existing solutions: - * - SSR-capable: it shows initial value during SSR and hydration, but immediately - * initializes when clientside mounted. - * - Sync state across tabs: When another tab changes the value in the storage area, the - * current tab follows suit. - */ -function useStorageStateBrowser( - kind: 'session' | 'local', - key: string, - initializer: string | Initializer, -): UseStorageStateHookResult; -function useStorageStateBrowser( - kind: 'session' | 'local', - key: string, - initializer?: string | null | Initializer, -): UseStorageStateHookResult; -function useStorageStateBrowser( - kind: 'session' | 'local', - key: string, - initializer: string | null | Initializer = null, -): UseStorageStateHookResult | UseStorageStateHookResult { - const [initialValue] = React.useState(initializer); - const area = kind === 'session' ? window.sessionStorage : window.localStorage; - const subscribeKey = React.useCallback((cb: () => void) => subscribe(area, key, cb), [area, key]); - const getKeySnapshot = React.useCallback( - () => getSnapshot(area, key) ?? initialValue, - [area, initialValue, key], - ); - const getKeyServerSnapshot = React.useCallback(() => initialValue, [initialValue]); - - const storedValue = React.useSyncExternalStore( - subscribeKey, - getKeySnapshot, - getKeyServerSnapshot, - ); - - const setStoredValue = React.useCallback( - (value: React.SetStateAction) => { - const valueToStore = value instanceof Function ? value(storedValue) : value; - setValue(area, key, valueToStore); - }, - [area, key, storedValue], - ); - - return [storedValue, setStoredValue]; -} - -export default typeof window === 'undefined' ? useStorageStateServer : useStorageStateBrowser; diff --git a/packages/toolpad-utils/src/http.ts b/packages/toolpad-utils/src/http.ts deleted file mode 100644 index d5862ca0775..00000000000 --- a/packages/toolpad-utils/src/http.ts +++ /dev/null @@ -1,37 +0,0 @@ -import * as http from 'http'; -import invariant from 'invariant'; - -/** - * A Promise wrapper for server.listen - */ -export async function listen(handler: http.RequestListener | http.Server, port?: number) { - const server = typeof handler === 'function' ? http.createServer(handler) : handler; - let app: http.Server | undefined; - await new Promise((resolve, reject) => { - app = server.listen(port); - app.once('listening', resolve); - app.once('error', reject); - }); - - const address = app?.address(); - invariant(address && typeof address === 'object', 'expected address to be an AddressInfo object'); - - return { - port: address.port, - async close() { - await new Promise((resolve, reject) => { - if (app) { - app.close((err) => { - if (err) { - reject(err); - } else { - resolve(); - } - }); - } else { - resolve(); - } - }); - }, - }; -} diff --git a/packages/toolpad-utils/src/httpApiAdapters.ts b/packages/toolpad-utils/src/httpApiAdapters.ts deleted file mode 100644 index 72f1cc30c34..00000000000 --- a/packages/toolpad-utils/src/httpApiAdapters.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type express from 'express'; - -export function encodeRequestBody(req: express.Request) { - const contentType = req.headers['content-type']; - - if (typeof req.body === 'object' && contentType?.includes('application/x-www-form-urlencoded')) { - return Object.entries(req.body as Record).reduce( - (acc, [key, value]) => { - const encKey = encodeURIComponent(key); - const encValue = encodeURIComponent(value); - return `${acc ? `${acc}&` : ''}${encKey}=${encValue}`; - }, - '', - ); - } - - if (contentType?.includes('application/json')) { - return JSON.stringify(req.body); - } - - return req.body; -} - -export function adaptRequestFromExpressToFetch(req: express.Request) { - // Converting Express req headers to Fetch API's Headers - const headers = new Headers(); - for (const headerName of Object.keys(req.headers)) { - const headerValue: string = req.headers[headerName]?.toString() ?? ''; - if (Array.isArray(headerValue)) { - for (const value of headerValue) { - headers.append(headerName, value); - } - } else { - headers.append(headerName, headerValue); - } - } - - // Creating Fetch API's Request object from Express' req - return new Request(`${req.protocol}://${req.get('host')}${req.originalUrl}`, { - method: req.method, - headers, - body: /GET|HEAD/.test(req.method) ? undefined : encodeRequestBody(req), - }); -} diff --git a/packages/toolpad-utils/src/immutability.ts b/packages/toolpad-utils/src/immutability.ts deleted file mode 100644 index bdbd4d73482..00000000000 --- a/packages/toolpad-utils/src/immutability.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * Applies changes to an object in an immutable way. The `dest` object will adopt the properties of - * the `src` object. Object identity is preserved if the operation results in a no-op. - */ -export function update(dest: T, src: Partial): T { - let result: T | undefined; - Object.entries(src).forEach(([key, value]) => { - if (dest[key as keyof T] !== value) { - result = result || { ...dest }; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (result as any)[key] = value; - } - }); - return result || dest; -} -/** - * Applies changes to an object in an immutable way. The `dest` object will adopt the properties of - * the `src` object. If `dest` is undefined, `src` will be used. Object identity is preserved if - * the operation results in a no-op. - */ -export function updateOrCreate(dest: T | null | undefined, src: NonNullable): T { - return dest ? update(dest, src) : src; -} - -/** - * Inserts a value in an immutable array. - */ -export function insert(array: readonly T[], value: T, index: number): T[] { - return [...array.slice(0, index), value, ...array.slice(index)]; -} - -/** - * Updates a value in an immutable array. - */ -export function updateArray(array: readonly T[], value: T, index: number): T[] { - return [...array.slice(0, index), value, ...array.slice(index + 1)]; -} - -/** - * Removes a value in an immutable array. - */ -export function remove(array: readonly T[], index: number): T[] { - return [...array.slice(0, index), ...array.slice(index + 1)]; -} - -/** - * Removes a set of properties from an object in an immutable way. Object identity is preserved if - * the operation results in a no-op. - */ -export function omit(obj: T, ...keys: readonly K[]): Omit { - let result: T | undefined; - - keys.forEach((key) => { - if (Object.prototype.hasOwnProperty.call(obj, key)) { - if (!result) { - result = { ...obj }; - } - delete result[key]; - } - }); - - return result || obj; -} - -/** - * Returns an object created from `obj` with only the specified `keys`. Object identity is preserved if - * the operation results in a no-op. - */ -export function take>( - obj: T, - ...keys: readonly K[] -): Omit> { - const keySet = new Set(keys); - let result: T | undefined; - - Object.keys(obj).forEach((key) => { - if (!keySet.has(key)) { - if (!result) { - result = { ...obj }; - } - delete result[key as keyof T]; - } - }); - - return result || obj; -} -/** - * Returns an array without any of its items equal to `value`. Object identity is preserved if - * the operation results in a no-op. - */ -export function without(array: readonly T[], value: T): readonly T[] { - const result: T[] = []; - - let found = false; - for (let i = 0; i < array.length; i += 1) { - const elm = array[i]; - if (elm === value) { - found = true; - } else { - result.push(elm); - } - } - - return found ? result : array; -} diff --git a/packages/toolpad-utils/src/json.ts b/packages/toolpad-utils/src/json.ts deleted file mode 100644 index 2a3396ca1d3..00000000000 --- a/packages/toolpad-utils/src/json.ts +++ /dev/null @@ -1,44 +0,0 @@ -export type Replacer = (this: object, key: PropertyKey, value: unknown) => unknown; - -// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value#circular_references -export function getCircularReplacer(): Replacer { - const ancestors: object[] = []; - return function replacer(key, value) { - if (typeof value !== 'object' || value === null) { - return value; - } - // `this` is the object that value is contained in, - // i.e., its direct parent. - while (ancestors.length > 0 && ancestors.at(-1) !== this) { - ancestors.pop(); - } - if (ancestors.includes(value)) { - return '[Circular]'; - } - ancestors.push(value); - return value; - }; -} - -function replaceRecursiveImpl(obj: unknown, replacer: Replacer): unknown { - if (Array.isArray(obj)) { - return obj.map((item, i) => { - const newItem = replacer.call(obj, i, item); - return replaceRecursiveImpl(newItem, replacer); - }); - } - if (obj && typeof obj === 'object') { - return Object.fromEntries( - Object.entries(obj).map(([key, value]) => { - const newValue = replacer.call(obj, key, value); - return [key, replaceRecursiveImpl(newValue, replacer)]; - }), - ); - } - return obj; -} - -// Replaces nested properties using the same semantics as JSON.stringify -export function replaceRecursive(obj: unknown, replacer: Replacer): unknown { - return (replaceRecursiveImpl({ '': obj }, replacer) as { '': unknown })['']; -} diff --git a/packages/toolpad-utils/src/objectKey.spec.ts b/packages/toolpad-utils/src/objectKey.spec.ts deleted file mode 100644 index be26af70e75..00000000000 --- a/packages/toolpad-utils/src/objectKey.spec.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { getObjectKey } from './objectKey'; - -const OBJECT1 = {}; -const OBJECT2 = {}; - -describe('getObjectKey', () => { - test('compare the same object', () => { - expect(getObjectKey(OBJECT1)).toBe(getObjectKey(OBJECT1)); - }); - - test('compare different object', () => { - expect(getObjectKey(OBJECT1)).not.toBe(getObjectKey(OBJECT2)); - }); -}); diff --git a/packages/toolpad-utils/src/objectKey.ts b/packages/toolpad-utils/src/objectKey.ts deleted file mode 100644 index 730fce68ced..00000000000 --- a/packages/toolpad-utils/src/objectKey.ts +++ /dev/null @@ -1,20 +0,0 @@ -const weakMap = new WeakMap(); -let nextId = 0; - -function getNextId(): string { - const id = `object-id::${nextId}`; - nextId += 1; - return id; -} - -/** - * Used to generate ids for object instances. - */ -export function getObjectKey(object: object): string { - let id = weakMap.get(object); - if (!id) { - id = getNextId(); - weakMap.set(object, id); - } - return id; -} diff --git a/packages/toolpad-utils/src/path.spec.ts b/packages/toolpad-utils/src/path.spec.ts deleted file mode 100644 index d8f3ef12aee..00000000000 --- a/packages/toolpad-utils/src/path.spec.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { test, expect } from 'vitest'; -import { getExtension } from './path'; - -test.each([ - ['/a/b/c.foo', '.foo'], - ['/a/b/c', ''], - ['a', ''], - ['a.foo', '.foo'], - ['/a/b.foo.bar', '.bar'], - ['b.foo.bar', '.bar'], -])('extension of %s should be %s', async (got, expected) => { - expect(getExtension(got)).toEqual(expected); -}); diff --git a/packages/toolpad-utils/src/path.ts b/packages/toolpad-utils/src/path.ts deleted file mode 100644 index 4766c9c31e7..00000000000 --- a/packages/toolpad-utils/src/path.ts +++ /dev/null @@ -1,12 +0,0 @@ -const IMAGE_EXTENSIONS = ['.jpg', '.jpeg', '.png', '.gif', '.svg', '.webp']; - -export function getExtension(filePath: string): string { - const fileName = filePath.split('/').pop() || ''; - const dotIndex = fileName.lastIndexOf('.'); - return dotIndex < 0 ? '' : fileName.substring(dotIndex); -} - -export function hasImageExtension(pathName: string): boolean { - const extension = getExtension(pathName); - return IMAGE_EXTENSIONS.includes(extension); -} diff --git a/packages/toolpad-utils/src/promises.spec.ts b/packages/toolpad-utils/src/promises.spec.ts deleted file mode 100644 index 0a6a5746193..00000000000 --- a/packages/toolpad-utils/src/promises.spec.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { resolveValues } from './promises'; - -describe('resolveValues', () => { - test.each([ - [ - { - a: 1, - b: Promise.resolve(2), - }, - { - a: 1, - b: 2, - }, - ], - [{}, {}], - ])('should resolve %p to %p', async (got, expected) => { - expect(await resolveValues(got)).toEqual(expected); - }); -}); diff --git a/packages/toolpad-utils/src/promises.ts b/packages/toolpad-utils/src/promises.ts deleted file mode 100644 index 17a0e53133e..00000000000 --- a/packages/toolpad-utils/src/promises.ts +++ /dev/null @@ -1,9 +0,0 @@ -type AwaitedProps

= { [K in keyof P]: Awaited }; - -/** - * Returns a Promise to an object with all the properties resolved as promises - */ -export async function resolveValues

(obj: P): Promise> { - const entries = Object.entries(obj).map(async ([key, value]) => [key, await value]); - return Object.fromEntries(await Promise.all(entries)) as AwaitedProps

; -} diff --git a/packages/toolpad-utils/src/react.spec.tsx b/packages/toolpad-utils/src/react.spec.tsx deleted file mode 100644 index a7f8290493e..00000000000 --- a/packages/toolpad-utils/src/react.spec.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/** - * @vitest-environment jsdom - */ - -import * as React from 'react'; -import { cleanup, render, screen } from '@testing-library/react'; -import { test, expect, afterEach } from 'vitest'; -import { interleave } from './react'; -import 'vitest-dom/extend-expect'; - -afterEach(cleanup); - -test('handles empty', async () => { - render(

{interleave([],
quux
)}
); - expect(await screen.findByTestId('container')).toBeEmptyDOMElement(); -}); - -test('handles single item', async () => { - render( -
{interleave([
foo
],
quux
)}
, - ); - expect(await screen.findByTestId('container')).toContainHTML('
foo
'); -}); - -test('interleaves multiple items', async () => { - render( -
- {interleave( - [
foo
,
bar
,
baz
], -
quux
, - )} -
, - ); - - expect(await screen.findByTestId('container')).toContainHTML( - '
foo
quux
bar
quux
baz
', - ); -}); - -test('interleaves strings', async () => { - render(
{interleave(['foo', 'bar', 'baz'],
quux
)}
); - - expect(await screen.findByTestId('container')).toContainHTML( - 'foo
quux
bar
quux
baz', - ); -}); - -test('interleaves string separator', async () => { - render( -
- {interleave([
foo
,
bar
,
baz
], 'quux')} -
, - ); - - expect(await screen.findByTestId('container')).toContainHTML( - '
foo
quux
bar
quux
baz
', - ); -}); diff --git a/packages/toolpad-utils/src/react.tsx b/packages/toolpad-utils/src/react.tsx deleted file mode 100644 index dc5ed6a60a5..00000000000 --- a/packages/toolpad-utils/src/react.tsx +++ /dev/null @@ -1,125 +0,0 @@ -import * as React from 'react'; -import * as ReactIs from 'react-is'; - -/** - * Like `Array.prototype.join`, but for React nodes. - */ -export function interleave(items: React.ReactNode[], separator: React.ReactNode): React.ReactNode { - const result: React.ReactNode[] = []; - - for (let i = 0; i < items.length; i += 1) { - if (i > 0) { - if (ReactIs.isElement(separator)) { - result.push(React.cloneElement(separator as React.ReactElement, { key: `separator-${i}` })); - } else { - result.push(separator); - } - } - - const item = items[i]; - result.push(item); - } - - return {result}; -} - -/** - * Consume a context but throw when used outside of a provider. - */ -export function useNonNullableContext(context: React.Context, name?: string): NonNullable { - const maybeContext = React.useContext(context); - if (maybeContext === null || maybeContext === undefined) { - throw new Error(`context "${name}" was used without a Provider`); - } - return maybeContext; -} - -/** - * Context that throws when used outside of a provider. - */ -export function createProvidedContext( - name?: string, -): [() => T, React.ComponentType>] { - const context = React.createContext(undefined); - const useContext = () => useNonNullableContext(context, name); - return [useContext, context.Provider as React.ComponentType>]; -} - -export function useAssertedContext(context: React.Context): T { - const value = React.useContext(context); - if (value === undefined) { - throw new Error('context was used without a Provider'); - } - return value; -} - -/** - * Debugging tool that logs updates to props. - */ -export function useTraceUpdates

(prefix: string, props: P) { - const prev = React.useRef

(props); - React.useEffect(() => { - const changedProps: Partial

= {}; - - for (const key of Object.keys(props) as (keyof P)[]) { - if (!Object.is(prev.current[key], props[key])) { - changedProps[key] = props[key]; - } - } - - if (Object.keys(changedProps).length > 0) { - // eslint-disable-next-line no-console - console.log(`${prefix} changed props:`, changedProps); - } - - prev.current = props; - }); -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export default function getComponentDisplayName(Component: React.ComponentType | string) { - if (typeof Component === 'string') { - return Component || 'Unknown'; - } - - return Component.displayName || Component.name; -} - -/** - * Create a shared state to be used across the application. Returns a useState hook that - * is synchronized on the same state between all instances where it is called. - */ -export function createGlobalState(initialState: T) { - let state = initialState; - const listeners: Array<(state: T) => void> = []; - - const subscribe = (cb: (state: T) => void) => { - listeners.push(cb); - return () => { - const index = listeners.indexOf(cb); - listeners.splice(index, 1); - }; - }; - - const getState = () => state; - - const setState = (newState: T | ((oldValue: T) => T)) => { - state = typeof newState === 'function' ? (newState as Function)(state) : newState; - listeners.forEach((cb) => cb(state)); - }; - - const useValue = () => React.useSyncExternalStore(subscribe, getState, getState); - - const useState = (): [T, React.Dispatch>] => { - const value = useValue(); - return [value, setState]; - }; - - return { - getState, - setState, - useValue, - useState, - subscribe, - }; -} diff --git a/packages/toolpad-utils/src/strings.spec.ts b/packages/toolpad-utils/src/strings.spec.ts deleted file mode 100644 index d9e067eded9..00000000000 --- a/packages/toolpad-utils/src/strings.spec.ts +++ /dev/null @@ -1,136 +0,0 @@ -import { describe, test, expect } from 'vitest'; -import { - findImports, - capitalize, - uncapitalize, - pascalCase, - camelCase, - guessTitle, - kebabToConstant, - kebabToPascal, -} from './strings'; - -describe('findImports', () => { - test('finds all imports', () => { - const imports = findImports(` -import { - Component -} from '@angular2/core'; -import defaultMember from "module-1"; -import * as name from "module-2 "; -import { member } from " module-3"; -import { member as alias } from "module-4"; -import { member1 , -member2 } from "module-5"; -import { member1 , member2 as alias2 , member3 as alias3 } from "module-6"; -import defaultMember, { member, member } from "module-7"; -import defaultMember, * as name from "module-8"; -import "module-9"; - import "module-10"; -import * from './smdn'; - `); - - expect(imports[0]).toBe('@angular2/core'); - expect(imports[1]).toBe('module-1'); - expect(imports[2]).toBe('module-2 '); - expect(imports[3]).toBe('module-3'); - expect(imports[4]).toBe('module-4'); - expect(imports[5]).toBe('module-5'); - expect(imports[6]).toBe('module-6'); - expect(imports[7]).toBe('module-7'); - expect(imports[8]).toBe('module-8'); - expect(imports[9]).toBe('module-9'); - expect(imports[10]).toBe('module-10'); - expect(imports[11]).toBe('./smdn'); - }); -}); - -describe('capitalize', () => { - test.each([ - ['foo', 'Foo'], - ['FOO', 'FOO'], - ['fOO', 'FOO'], - ['', ''], - ['a', 'A'], - ['A', 'A'], - ['-', '-'], - ])('should convert %p to %p', (got, expected) => { - expect(capitalize(got)).toEqual(expected); - }); -}); - -describe('uncapitalize', () => { - test.each([ - ['foo', 'foo'], - ['FOO', 'fOO'], - ['fOO', 'fOO'], - ['', ''], - ['a', 'a'], - ['A', 'a'], - ['-', '-'], - ])('should convert %p to %p', (got, expected) => { - expect(uncapitalize(got)).toEqual(expected); - }); -}); - -describe('pascalCase', () => { - test.each([ - [['foo'], 'Foo'], - [['foo', 'bar'], 'FooBar'], - [['FOO', 'BAR'], 'FooBar'], - [['foo', '-', 'bar'], 'Foo-Bar'], - [[''], ''], - ])('should convert %p to %p', (got, expected) => { - expect(pascalCase(...got)).toEqual(expected); - }); -}); - -describe('camelCase', () => { - test.each([ - [['foo'], 'foo'], - [['foo', 'bar'], 'fooBar'], - [['foo', '-', 'bar'], 'foo-Bar'], - [['foo', 'bar', 'baz'], 'fooBarBaz'], - [[''], ''], - ])('should convert %p to %p', (got, expected) => { - expect(camelCase(...got)).toEqual(expected); - }); -}); - -describe('kebabToConstant', () => { - test.each([ - ['foo-bar', 'FOO_BAR'], - ['foo-bar-baz', 'FOO_BAR_BAZ'], - ['foo', 'FOO'], - ['foo-bar-baz-qux', 'FOO_BAR_BAZ_QUX'], - ])('should convert %p to %p', (got, expected) => { - expect(kebabToConstant(got)).toEqual(expected); - }); -}); - -describe('kebabToPascalCase', () => { - test.each([ - ['foo-bar', 'FooBar'], - ['foo-bar-baz', 'FooBarBaz'], - ['foo', 'Foo'], - ['foo-bar-baz-qux', 'FooBarBazQux'], - ])('should convert %p to %p', (got, expected) => { - expect(kebabToPascal(got)).toEqual(expected); - }); -}); - -describe('guessTitle', () => { - test.each([ - ['camelCaseExample', 'Camel Case Example'], - ['snake_case_example', 'Snake Case Example'], - ['kebab-case-example', 'Kebab Case Example'], - ['ACRONYMExample', 'Acronym Example'], - ['helloACRONYMExample', 'Hello Acronym Example'], - ['HelloACRONYMExample', 'Hello Acronym Example'], - ['example123', 'Example 123'], - ['example123Wat', 'Example 123 Wat'], - ['example123more456', 'Example 123 More 456'], - ])('should split %p into %p', (got, expected) => { - expect(guessTitle(got)).toEqual(expected); - }); -}); diff --git a/packages/toolpad-utils/src/strings.ts b/packages/toolpad-utils/src/strings.ts deleted file mode 100644 index 9ca3b4e6c9e..00000000000 --- a/packages/toolpad-utils/src/strings.ts +++ /dev/null @@ -1,209 +0,0 @@ -import title from 'title'; - -/** - * Makes the first letter of [str] uppercase. - * Not locale aware. - */ -export function uncapitalize(str: string): string { - return str.length > 0 ? str[0].toLowerCase() + str.slice(1) : ''; -} - -/** - * Makes the first letter of [str] lowercase. - * Not locale aware. - */ -export function capitalize(str: string): string { - return str.length > 0 ? str[0].toUpperCase() + str.slice(1) : ''; -} - -/** - * Capitalizes and joins all [parts]. - */ -export function pascalCase(...parts: string[]): string { - return parts.map((part) => capitalize(part.toLowerCase())).join(''); -} - -/** - * Joins all [parts] and camelcases the result - */ -export function camelCase(...parts: string[]): string { - if (parts.length > 0) { - const [first, ...rest] = parts; - return uncapitalize(first) + pascalCase(...rest); - } - return ''; -} - -/** - * Turns a kebab-case string into a constant case string. - */ -export function kebabToConstant(input: string): string { - return input - .split('-') - .map((part) => part.toUpperCase()) - .join('_'); -} - -/** - * Turns a kebab-case string into a PascalCase string. - */ -export function kebabToPascal(input: string): string { - return input - .split('-') - .map((part) => capitalize(part)) - .join(''); -} - -/** - * Generates a string for `base` by add a number until it's unique amongst a set of predefined names. - */ -export function generateUniqueString(base: string, existingNames: Set) { - let i = 1; - if (!existingNames.has(base)) { - return base; - } - const newBase = base.replace(/\d+$/, ''); - let suggestion = newBase; - while (existingNames.has(suggestion)) { - suggestion = newBase + String(i); - i += 1; - } - return suggestion; -} - -/** - * Escape string for use in HTML. - */ -export function escapeHtml(unsafe: string): string { - return unsafe - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, '''); -} - -/** - * Normalizes and removes all diacritics from a javascript string. - * - * See https://stackoverflow.com/a/37511463 - */ -export function removeDiacritics(input: string): string { - return input.normalize('NFD').replace(/[\u0300-\u036f]/g, ''); -} - -export function isAbsoluteUrl(maybeUrl: string) { - try { - return !!new URL(maybeUrl); - } catch { - return false; - } -} - -/** - * Removes a prefix from a string if it starts with it. - */ -export function removePrefix(input: string, prefix: string): string { - return input.startsWith(prefix) ? input.slice(prefix.length) : input; -} - -/** - * Removes a suffix from a string if it ends with it. - */ -export function removeSuffix(input: string, suffix: string): string { - return input.endsWith(suffix) ? input.slice(0, -suffix.length) : input; -} - -/** - * Adds a prefix to a string if it doesn't start with it. - */ -export function ensurePrefix(input: string, prefix: string): string { - return input.startsWith(prefix) ? input : prefix + input; -} - -/** - * Adds a suffix to a string if it doesn't end with it. - */ -export function ensureSuffix(input: string, suffix: string): string { - return input.endsWith(suffix) ? input : input + suffix; -} - -/** - * Regex to statically find all static import statements - * - * Tested against: - * import { - * Component - * } from '@angular2/core'; - * import defaultMember from "module-name"; - * import * as name from "module-name "; - * import { member } from " module-name"; - * import { member as alias } from "module-name"; - * import { member1 , - * member2 } from "module-name"; - * import { member1 , member2 as alias2 , member3 as alias3 } from "module-name"; - * import defaultMember, { member, member } from "module-name"; - * import defaultMember, * as name from "module-name"; - * import "module-name"; - * import * from './smdn'; - */ -const IMPORT_STATEMENT_REGEX = - /^\s*import(?:["'\s]*([\w*{}\n, ]+)from\s*)?["'\s]*([^"']+)["'\s].*/gm; - -/** - * Statically analyses a javascript source code for import statements and return the specifiers. - * - * NOTE: This function does a best effort without parsing the code. The result may contain false - * positives - */ -export function findImports(src: string): string[] { - return Array.from(src.matchAll(IMPORT_STATEMENT_REGEX), (match) => match[2]); -} - -/** - * Limits the length of a string and adds ellipsis if necessary. - */ -export function truncate(str: string, maxLength: number, dots: string = '...') { - if (str.length <= maxLength) { - return str; - } - return str.slice(0, maxLength) + dots; -} - -/** - * Prepend a prefix to each line in the text - */ -export function prependLines(text: string, prefix: string): string { - return text - .split('\n') - .map((line) => prefix + line) - .join('\n'); -} - -/** - * Indent the text with [length] number of spaces - */ -export function indent(text: string, length = 2): string { - return prependLines(text, ' '.repeat(length)); -} - -/** - * Returns true if the string is a valid javascript identifier - */ -export function isValidJsIdentifier(base: string): boolean { - return /^[a-zA-Z_][a-zA-Z0-9_]*$/.test(base); -} - -export function guessTitle(str: string): string { - // Replace snake_case with space - str = str.replace(/[_-]/g, ' '); - // Split camelCase - str = str.replace(/([a-z0-9])([A-Z])/g, '$1 $2'); - // Split acronyms - str = str.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2'); - // Split numbers - str = str.replace(/([a-zA-Z])(\d+)/g, '$1 $2'); - str = str.replace(/(\d+)([a-zA-Z])/g, '$1 $2'); - - return title(str); -} diff --git a/packages/toolpad-utils/src/types.ts b/packages/toolpad-utils/src/types.ts deleted file mode 100644 index 36af6f0ef9f..00000000000 --- a/packages/toolpad-utils/src/types.ts +++ /dev/null @@ -1,98 +0,0 @@ -declare const brand: unique symbol; - -export type WithControlledProp< - V, - K extends string = 'value', - O extends string = K extends 'value' ? 'onChange' : `on${Capitalize}Change`, -> = Record & Record void>; - -export type ExactEntriesOf

= Exclude<{ [K in keyof P]: [K, P[K]] }[keyof P], undefined>[]; - -/** - * The inverse of Awaited. - */ -export type Awaitable = T | Promise | PromiseLike; - -/** - * @example - * type T0 = Join<[1, 2, 3, 4], '.'>; // '1.2.3.4' - * type T1 = Join<['foo', 'bar', 'baz'], '-'>; // 'foo-bar-baz' - * type T2 = Join<[], '.'>; // '' - */ -export type Join = T extends [] - ? '' - : T extends [string | number | boolean | bigint] - ? `${T[0]}` - : T extends [string | number | boolean | bigint, ...infer U] - ? `${T[0]}${D}${Join}` - : string; - -/** - * @example - * type T0 = Split<'foo', '.'>; // ['foo'] - * type T1 = Split<'foo.bar.baz', '.'>; // ['foo', 'bar', 'baz'] - * type T2 = Split<'foo.bar', ''>; // ['f', 'o', 'o', '.', 'b', 'a', 'r'] - */ -export type Split = string extends S - ? string[] - : S extends '' - ? [] - : S extends `${infer T}${D}${infer U}` - ? [T, ...Split] - : [S]; - -/** - * @example - * type T0 = CapitalizeAll<['foo', 'bar']>; // ['Foo', 'Bar'] - * type T1 = CapitalizeAll<[]>; // [] - */ -export type CapitalizeAll = T extends [] - ? [] - : T extends [string, ...infer U] - ? U extends string[] - ? [Capitalize, ...CapitalizeAll] - : never - : never; - -/** - * @example - * type T0 = CapitalizeAll<['foo', 'bar', 'baz']>; // ['foo', 'Bar', 'Baz'] - * type T1 = CapitalizeAll<['foo']>; // ['foo'] - * type T2 = CapitalizeAll<[]>; // [] - */ -export type CapitalizeTail = T extends [] - ? [] - : T extends [string, ...infer U] - ? U extends string[] - ? [T[0], ...CapitalizeAll] - : never - : never; - -/** - * sString template type that converts snake-case to camel-case - * @example - * type T0 = SnakeToCamel<'foo-bar-baz'>; // 'fooBarBaz' - * type T1 = CapitalizeAll<'foo'>; // 'foo' - * type T2 = CapitalizeAll<''>; // '' - */ -export type SnakeToCamel = Join>, ''>; - -/** - * The inverso of NonNullable - */ -export type Maybe = T | undefined | null; - -export type ValueOf = T[keyof T]; - -export interface Brand { - readonly [brand]: B; -} - -// https://stackoverflow.com/a/56749647 -export type Branded = A & Brand; - -// See https://github.com/microsoft/vscode/issues/94679#issuecomment-755194161 -export type Expand = T extends infer O ? { [K in keyof O]: O[K] } : never; -export type ExpandNested = T extends infer O ? { [K in keyof O]: Expand } : never; - -export {}; diff --git a/packages/toolpad-utils/src/warnOnce.ts b/packages/toolpad-utils/src/warnOnce.ts deleted file mode 100644 index d78b2645ee7..00000000000 --- a/packages/toolpad-utils/src/warnOnce.ts +++ /dev/null @@ -1,12 +0,0 @@ -const history = new Set(); - -/** - * Warns only once for a specific message. - * @param msg The message to warn. - */ -export default function warnOnce(msg: string) { - if (!history.has(msg)) { - history.add(msg); - console.warn(msg); - } -} diff --git a/packages/toolpad-utils/src/workerRpc.ts b/packages/toolpad-utils/src/workerRpc.ts deleted file mode 100644 index 9001d074db0..00000000000 --- a/packages/toolpad-utils/src/workerRpc.ts +++ /dev/null @@ -1,114 +0,0 @@ -import { MessagePort, MessageChannel } from 'worker_threads'; -import { Awaitable } from './types'; -import { errorFrom, serializeError } from './errors'; - -/** - * Helpers that are intended to set up rpc between a Node.js worker thread and the main thread. - * Create the worker and pass a port in the workerData. - * - * On the main thread: - * - * const rpcChannel = new MessageChannel() - * const worker = new Worker('./myWorker.js', { - * workerData: { rpcPort: rpcChannel.port1 }, - * transferList: [rpcChannel.port1] - * }) - * - * // Depending of the direction of communication, either - * const client = createRpcClient(rpcChannel.port2) - * // or - * serveRpc(rpcChannel.port2, { - * myMethod - * }) - * - * On the worker thread: - * - * // Depending of the direction of communication, either - * const client = createRpcClient(workerData.rpcPort) - * // or - * serveRpc(workerData.rpcPort, { - * myMethod - * }) - * - * Use multiple channels for bidirectional communication. - */ - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export type Methods = Record Awaitable>; - -type MessageRequest = { - method: string; - args: unknown[]; - port: MessagePort; -}; - -interface MsgResponse { - error?: unknown; - result?: T; -} - -interface CreateRpcClientOptions { - timeout?: number; -} - -export function createRpcClient( - port: MessagePort, - { timeout = 30000 }: CreateRpcClientOptions = {}, -): M { - return new Proxy({} as M, { - get: (target, prop) => { - if (typeof prop !== 'string') { - return Reflect.get(target, prop); - } - return (...args: unknown[]) => { - return new Promise((resolve, reject) => { - const { port1, port2 } = new MessageChannel(); - - const timeoutId = setTimeout(() => { - port1.close(); - }, timeout); - - port1.on('message', (msg: MsgResponse) => { - clearTimeout(timeoutId); - if (msg.error) { - reject(msg.error); - } else { - resolve(msg.result); - } - }); - - port1.start(); - - port.postMessage( - { - method: prop, - args, - port: port2, - } satisfies MessageRequest, - [port2], - ); - }); - }; - }, - }); -} - -export function serveRpc(port: MessagePort, methods: M) { - const methodMap = new Map(Object.entries(methods)); - port.on('message', async (msg: MessageRequest) => { - const method = methodMap.get(msg.method); - if (method) { - try { - const result = await method(...msg.args); - msg.port.postMessage({ result } satisfies MsgResponse); - } catch (rawError) { - msg.port.postMessage({ error: serializeError(errorFrom(rawError)) } satisfies MsgResponse); - } - } else { - msg.port.postMessage({ - error: new Error(`Method "${msg.method}" not found`), - } satisfies MsgResponse); - } - }); - port.start(); -} diff --git a/packages/toolpad-utils/tsconfig.build.json b/packages/toolpad-utils/tsconfig.build.json deleted file mode 100644 index 26f64477258..00000000000 --- a/packages/toolpad-utils/tsconfig.build.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - // This config is for emitting declarations (.d.ts) only - // Actual .ts source files are transpiled via babel - "extends": "./tsconfig.json", - "compilerOptions": { - "declaration": true, - "noEmit": false, - "emitDeclarationOnly": true, - "outDir": "build/esm" - }, - "exclude": ["src/**/*.spec.ts*", "src/**/*.test.ts*"] -} diff --git a/packages/toolpad-utils/tsconfig.json b/packages/toolpad-utils/tsconfig.json deleted file mode 100644 index 7caeb665fef..00000000000 --- a/packages/toolpad-utils/tsconfig.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./build/tsconfig.json.tsbuildinfo", - "module": "ESNext", - "moduleResolution": "bundler", - "alwaysStrict": true, - "rootDir": "src", - "outDir": "build", - "declaration": true, - "target": "ES2020", - "skipLibCheck": true, - "pretty": true - }, - "include": ["src/**/*.ts", "src/**/*.tsx"] -} diff --git a/packages/toolpad-utils/vitest.config.mts b/packages/toolpad-utils/vitest.config.mts deleted file mode 100644 index 2fbb9725f3a..00000000000 --- a/packages/toolpad-utils/vitest.config.mts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - test: { - setupFiles: ['../../test/setupVitest.ts'], - coverage: { - exclude: ['./dist/**'], - reportsDirectory: './.coverage', - reporter: ['text', 'lcov'], - }, - }, -}); diff --git a/playground/nextjs-pages/.eslintrc.json b/playground/nextjs-pages/.eslintrc.json deleted file mode 100644 index bffb357a712..00000000000 --- a/playground/nextjs-pages/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/playground/nextjs-pages/README.md b/playground/nextjs-pages/README.md deleted file mode 100644 index 6a9c9eec920..00000000000 --- a/playground/nextjs-pages/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Toolpad Core Playground - Next.js Pages Router - -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/playground/nextjs-pages/next-env.d.ts b/playground/nextjs-pages/next-env.d.ts deleted file mode 100644 index 3cd7048ed94..00000000000 --- a/playground/nextjs-pages/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/playground/nextjs-pages/next.config.mjs b/playground/nextjs-pages/next.config.mjs deleted file mode 100644 index 38a959cb3e2..00000000000 --- a/playground/nextjs-pages/next.config.mjs +++ /dev/null @@ -1,8 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = { - reactStrictMode: true, - // https://github.com/nextauthjs/next-auth/discussions/9385#discussioncomment-8875108 - transpilePackages: ['next-auth'], -}; - -export default nextConfig; diff --git a/playground/nextjs-pages/package.json b/playground/nextjs-pages/package.json deleted file mode 100644 index 5ea114db676..00000000000 --- a/playground/nextjs-pages/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "playground-nextjs-pages", - "version": "0.13.0", - "private": true, - "scripts": { - "dev": "next dev", - "lint": "next lint" - }, - "devDependencies": { - "@emotion/react": "11.14.0", - "@emotion/styled": "11.14.0", - "@mui/icons-material": "6.4.7", - "@mui/material": "6.4.7", - "@mui/material-nextjs": "6.4.3", - "@toolpad/core": "workspace:*", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "eslint-config-next": "15.2.3", - "next": "^15.2.3", - "next-auth": "5.0.0-beta.25", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "zod": "3.24.2" - } -} diff --git a/playground/nextjs-pages/src/app/api/auth/[...nextauth]/route.ts b/playground/nextjs-pages/src/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index ca225652075..00000000000 --- a/playground/nextjs-pages/src/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { handlers } from '../../../../auth'; - -export const { GET, POST } = handlers; diff --git a/playground/nextjs-pages/src/auth.ts b/playground/nextjs-pages/src/auth.ts deleted file mode 100644 index b76886aa9ca..00000000000 --- a/playground/nextjs-pages/src/auth.ts +++ /dev/null @@ -1,77 +0,0 @@ -import NextAuth from 'next-auth'; -import GitHub from 'next-auth/providers/github'; -import Credentials from 'next-auth/providers/credentials'; -import type { Provider } from 'next-auth/providers'; - -const providers: Provider[] = [ - GitHub({ - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - }), - Credentials({ - credentials: { - email: { label: 'Email Address', type: 'email' }, - password: { label: 'Password', type: 'password' }, - }, - authorize(c) { - if (c.password !== 'password') { - return null; - } - return { - id: 'test', - name: 'Test User', - email: String(c.email), - }; - }, - }), -]; - -const missingVars: string[] = []; - -if (!process.env.GITHUB_CLIENT_ID) { - missingVars.push('GITHUB_CLIENT_ID'); -} -if (!process.env.GITHUB_CLIENT_SECRET) { - missingVars.push('GITHUB_CLIENT_SECRET'); -} - -if (missingVars.length > 0) { - const baseMessage = - 'Authentication is configured but the following environment variables are missing:'; - - if (process.env.NODE_ENV === 'production') { - throw new Error(`error - ${baseMessage} ${missingVars.join(', ')}`); - } else { - console.warn( - `\u001b[33mwarn\u001b[0m - ${baseMessage} \u001b[31m${missingVars.join(', ')}\u001b[0m`, - ); - } -} - -export const providerMap = providers.map((provider) => { - if (typeof provider === 'function') { - const providerData = provider(); - return { id: providerData.id, name: providerData.name }; - } - return { id: provider.id, name: provider.name }; -}); - -export const { handlers, auth } = NextAuth({ - providers, - secret: process.env.AUTH_SECRET, - pages: { - signIn: '/auth/signin', - }, - callbacks: { - authorized({ auth: session, request: { nextUrl } }) { - const isLoggedIn = !!session?.user; - const isPublicPage = nextUrl.pathname.startsWith('/public'); - - if (isPublicPage || isLoggedIn) { - return true; - } - - return false; // Redirect unauthenticated users to login page - }, - }, -}); diff --git a/playground/nextjs-pages/src/data/orders.ts b/playground/nextjs-pages/src/data/orders.ts deleted file mode 100644 index f0daa1ac650..00000000000 --- a/playground/nextjs-pages/src/data/orders.ts +++ /dev/null @@ -1,199 +0,0 @@ -'use client'; -import { DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud'; -import { z } from 'zod'; - -type OrderStatus = 'Pending' | 'Sent'; - -export interface Order extends DataModel { - id: number; - title: string; - description?: string; - status: OrderStatus; - itemCount: number; - fastDelivery: boolean; - createdAt: string; - deliveryTime?: string; -} - -const getOrdersStore = (): Order[] => { - const value = localStorage.getItem('orders-store'); - return value ? JSON.parse(value) : []; -}; - -const setOrdersStore = (value: Order[]) => { - return localStorage.setItem('orders-store', JSON.stringify(value)); -}; - -export const ordersDataSource: DataSource = { - fields: [ - { field: 'id', headerName: 'ID' }, - { field: 'title', headerName: 'Title' }, - { field: 'description', headerName: 'Description' }, - { - field: 'status', - headerName: 'Status', - type: 'singleSelect', - valueOptions: ['Pending', 'Sent'], - }, - { field: 'itemCount', headerName: 'No. of items', type: 'number' }, - { field: 'fastDelivery', headerName: 'Fast delivery', type: 'boolean' }, - { - field: 'createdAt', - headerName: 'Created at', - type: 'date', - valueGetter: (value: string) => value && new Date(value), - }, - { - field: 'deliveryTime', - headerName: 'Delivery time', - type: 'dateTime', - valueGetter: (value: string) => value && new Date(value), - }, - ], - getMany: async ({ paginationModel, filterModel, sortModel }) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - let filteredOrders = [...ordersStore]; - - // Apply filters (example only) - if (filterModel?.items?.length) { - filterModel.items.forEach(({ field, value, operator }) => { - if (!field || value == null) { - return; - } - - filteredOrders = filteredOrders.filter((order) => { - const orderValue = order[field]; - - switch (operator) { - case 'contains': - return String(orderValue).toLowerCase().includes(String(value).toLowerCase()); - case 'equals': - return orderValue === value; - case 'startsWith': - return String(orderValue).toLowerCase().startsWith(String(value).toLowerCase()); - case 'endsWith': - return String(orderValue).toLowerCase().endsWith(String(value).toLowerCase()); - case '>': - return (orderValue as number) > value; - case '<': - return (orderValue as number) < value; - default: - return true; - } - }); - }); - } - - // Apply sorting - if (sortModel?.length) { - filteredOrders.sort((a, b) => { - for (const { field, sort } of sortModel) { - if ((a[field] as number) < (b[field] as number)) { - return sort === 'asc' ? -1 : 1; - } - if ((a[field] as number) > (b[field] as number)) { - return sort === 'asc' ? 1 : -1; - } - } - return 0; - }); - } - - // Apply pagination - const start = paginationModel.page * paginationModel.pageSize; - const end = start + paginationModel.pageSize; - const paginatedOrders = filteredOrders.slice(start, end); - - return { - items: paginatedOrders, - itemCount: filteredOrders.length, - }; - }, - getOne: async (orderId) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - const orderToShow = ordersStore.find((order) => order.id === Number(orderId)); - - if (!orderToShow) { - throw new Error('Order not found'); - } - return orderToShow; - }, - createOne: async (data) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - const newOrder = { id: ordersStore.length + 1, ...data } as Order; - - setOrdersStore([...ordersStore, newOrder]); - - return newOrder; - }, - updateOne: async (orderId, data) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - let updatedOrder: Order | null = null; - - setOrdersStore( - ordersStore.map((order) => { - if (order.id === Number(orderId)) { - updatedOrder = { ...order, ...data }; - return updatedOrder; - } - return order; - }), - ); - - if (!updatedOrder) { - throw new Error('Order not found'); - } - return updatedOrder; - }, - deleteOne: async (orderId) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - setOrdersStore(ordersStore.filter((order) => order.id !== Number(orderId))); - }, - validate: z.object({ - title: z.string({ required_error: 'Title is required' }).nonempty('Title is required'), - description: z.string().optional(), - status: z.enum(['Pending', 'Sent'], { - errorMap: () => ({ message: 'Status must be "Pending" or "Sent"' }), - }), - itemCount: z - .number({ required_error: 'Item count is required' }) - .min(1, 'Item count must be at least 1'), - fastDelivery: z.boolean({ required_error: 'Fast delivery is required' }), - createdAt: z - .string({ required_error: 'Creation date is required' }) - .nonempty('Creation date is required'), - deliveryTime: z.string().optional(), - })['~standard'].validate, -}; - -export const ordersCache = new DataSourceCache(); diff --git a/playground/nextjs-pages/src/middleware.ts b/playground/nextjs-pages/src/middleware.ts deleted file mode 100644 index 02c48f4db60..00000000000 --- a/playground/nextjs-pages/src/middleware.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { auth as middleware } from './auth'; - -export const config = { - // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher - matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], -}; diff --git a/playground/nextjs-pages/src/pages/_app.tsx b/playground/nextjs-pages/src/pages/_app.tsx deleted file mode 100644 index 6ee56f6040b..00000000000 --- a/playground/nextjs-pages/src/pages/_app.tsx +++ /dev/null @@ -1,129 +0,0 @@ -import * as React from 'react'; -import { useRouter } from 'next/router'; -import { NextAppProvider } from '@toolpad/core/nextjs'; -import { PageContainer } from '@toolpad/core/PageContainer'; -import { DashboardLayout } from '@toolpad/core/DashboardLayout'; -import Head from 'next/head'; -import { AppCacheProvider } from '@mui/material-nextjs/v14-pagesRouter'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; -import type { NextPage } from 'next'; -import type { AppProps } from 'next/app'; -import type { Navigation } from '@toolpad/core/AppProvider'; -import { SessionProvider, signIn, signOut, useSession } from 'next-auth/react'; -import LinearProgress from '@mui/material/LinearProgress'; - -export type NextPageWithLayout

= NextPage & { - getLayout?: (page: React.ReactElement) => React.ReactNode; - requireAuth?: boolean; -}; - -type AppPropsWithLayout = AppProps & { - Component: NextPageWithLayout; -}; - -const NAVIGATION: Navigation = [ - { - kind: 'header', - title: 'Main items', - }, - { - title: 'Dashboard', - icon: , - }, - { - segment: 'orders', - title: 'Orders', - icon: , - pattern: 'orders{/:orderId}*', - }, -]; - -const BRANDING = { - title: 'My Toolpad Core Next.js Pages App', -}; - -const AUTHENTICATION = { - signIn, - signOut, -}; - -function DefaultLayout({ page }: { page: React.ReactElement }) { - const router = useRouter(); - const { segments = [] } = router.query; - const [orderId] = segments; - - const title = React.useMemo(() => { - if (router.asPath.split('?')[0] === '/orders/new') { - return 'New Order'; - } - if (orderId && router.asPath.includes('/edit')) { - return `Order ${orderId} - Edit`; - } - if (orderId) { - return `Order ${orderId}`; - } - return undefined; - }, [orderId, router.asPath]); - - return ( - - {page} - - ); -} - -function getDefaultLayout(page: React.ReactElement) { - return ; -} - -function RequireAuth({ children }: { children: React.ReactNode }) { - const { status } = useSession(); - - if (status === 'loading') { - return ; - } - - return children; -} - -function AppLayout({ children }: { children: React.ReactNode }) { - const { data: session } = useSession(); - return ( - - - - - - {children} - - - ); -} - -export default function App(props: AppPropsWithLayout) { - const { - Component, - pageProps: { session, ...pageProps }, - } = props; - - const getLayout = Component.getLayout ?? getDefaultLayout; - const requireAuth = Component.requireAuth ?? true; - - let pageContent = getLayout(); - if (requireAuth) { - pageContent = {pageContent}; - } - pageContent = {pageContent}; - - return ( - - {pageContent} - - ); -} diff --git a/playground/nextjs-pages/src/pages/_document.tsx b/playground/nextjs-pages/src/pages/_document.tsx deleted file mode 100644 index 89e61aa4167..00000000000 --- a/playground/nextjs-pages/src/pages/_document.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import * as React from 'react'; -import { Html, Head, Main, NextScript, DocumentProps, DocumentContext } from 'next/document'; -import { - DocumentHeadTags, - DocumentHeadTagsProps, - documentGetInitialProps, -} from '@mui/material-nextjs/v14-pagesRouter'; - -export default function Document(props: DocumentProps & DocumentHeadTagsProps) { - return ( - - - - - - -

- - - - ); -} - -Document.getInitialProps = async (ctx: DocumentContext) => { - const finalProps = await documentGetInitialProps(ctx); - return finalProps; -}; diff --git a/playground/nextjs-pages/src/pages/auth/signin.tsx b/playground/nextjs-pages/src/pages/auth/signin.tsx deleted file mode 100644 index de3ad580db8..00000000000 --- a/playground/nextjs-pages/src/pages/auth/signin.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import * as React from 'react'; -import type { GetServerSidePropsContext, InferGetServerSidePropsType } from 'next'; -import Link from '@mui/material/Link'; -import { SignInPage } from '@toolpad/core/SignInPage'; -import { signIn } from 'next-auth/react'; -import { useRouter } from 'next/router'; -import { auth, providerMap } from '../../auth'; - -function ForgotPasswordLink() { - return ( - - Forgot password? - - ); -} - -function SignUpLink() { - return Sign up; -} - -export default function SignIn({ - providers, -}: InferGetServerSidePropsType) { - const router = useRouter(); - return ( - { - try { - const signInResponse = await signIn( - provider.id, - formData - ? { - email: formData.get('email') as string, - password: formData.get('password') as string, - redirect: false, - } - : { callbackUrl: callbackUrl ?? '/' }, - ); - if (signInResponse && signInResponse.error) { - // Handle Auth.js errors - return { - error: - signInResponse.error === 'CredentialsSignin' - ? 'Invalid credentials' - : 'An error with Auth.js occurred', - type: signInResponse.error, - }; - } - // If the sign in was successful, - // manually redirect to the callback URL - // since the `redirect: false` option was used - // to be able to display error messages on the same page without a full page reload - if (provider.id === 'credentials') { - router.push(callbackUrl ?? '/'); - } - return {}; - } catch (error) { - // An error boundary must exist to handle unknown errors - return { - error: 'Something went wrong.', - type: 'UnknownError', - }; - } - }} - slots={{ forgotPasswordLink: ForgotPasswordLink, signUpLink: SignUpLink }} - /> - ); -} - -SignIn.getLayout = (page: React.ReactNode) => page; - -SignIn.requireAuth = false; - -export async function getServerSideProps(context: GetServerSidePropsContext) { - const session = await auth(context); - - // If the user is already logged in, redirect. - // Note: Make sure not to redirect to the same page - // To avoid an infinite loop! - if (session) { - return { redirect: { destination: '/' } }; - } - - return { - props: { - providers: providerMap, - }, - }; -} diff --git a/playground/nextjs-pages/src/pages/index.tsx b/playground/nextjs-pages/src/pages/index.tsx deleted file mode 100644 index a23c1d4f688..00000000000 --- a/playground/nextjs-pages/src/pages/index.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; -import Typography from '@mui/material/Typography'; -import { useSession } from 'next-auth/react'; - -export default function HomePage() { - const { data: session } = useSession(); - - return Welcome to Toolpad, {session?.user?.name || 'User'}!; -} - -HomePage.requireAuth = true; diff --git a/playground/nextjs-pages/src/pages/orders/[[...segments]].tsx b/playground/nextjs-pages/src/pages/orders/[[...segments]].tsx deleted file mode 100644 index 45d0f60513d..00000000000 --- a/playground/nextjs-pages/src/pages/orders/[[...segments]].tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import { Crud } from '@toolpad/core/Crud'; -import { ordersDataSource, Order, ordersCache } from '../../data/orders'; - -export default function OrdersCrudPage() { - return ( - - dataSource={ordersDataSource} - dataSourceCache={ordersCache} - rootPath="/orders" - initialPageSize={25} - defaultValues={{ itemCount: 1 }} - /> - ); -} diff --git a/playground/nextjs-pages/tsconfig.json b/playground/nextjs-pages/tsconfig.json deleted file mode 100644 index 2cb18e2f53b..00000000000 --- a/playground/nextjs-pages/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "target": "ES2017" - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/playground/nextjs/.eslintrc.json b/playground/nextjs/.eslintrc.json deleted file mode 100644 index bffb357a712..00000000000 --- a/playground/nextjs/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/playground/nextjs/.gitignore b/playground/nextjs/.gitignore deleted file mode 100644 index 68c5d18f00d..00000000000 --- a/playground/nextjs/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules/ -/test-results/ -/playwright-report/ -/blob-report/ -/playwright/.cache/ diff --git a/playground/nextjs/README.md b/playground/nextjs/README.md deleted file mode 100644 index 8c55e93a782..00000000000 --- a/playground/nextjs/README.md +++ /dev/null @@ -1,34 +0,0 @@ -# Toolpad Core Playground - Next.js App Router - -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/playground/nextjs/next-env.d.ts b/playground/nextjs/next-env.d.ts deleted file mode 100644 index 1b3be0840f3..00000000000 --- a/playground/nextjs/next-env.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -/// -/// - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/playground/nextjs/next.config.mjs b/playground/nextjs/next.config.mjs deleted file mode 100644 index 4678774e6d6..00000000000 --- a/playground/nextjs/next.config.mjs +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = {}; - -export default nextConfig; diff --git a/playground/nextjs/package.json b/playground/nextjs/package.json deleted file mode 100644 index 7a45d4a338a..00000000000 --- a/playground/nextjs/package.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "playground-nextjs", - "version": "0.13.0", - "private": true, - "scripts": { - "dev": "next dev", - "lint": "next lint" - }, - "devDependencies": { - "@emotion/react": "11.14.0", - "@emotion/styled": "11.14.0", - "@mui/icons-material": "6.4.7", - "@mui/material": "6.4.7", - "@mui/material-nextjs": "6.4.3", - "@toolpad/core": "workspace:*", - "@types/node": "^20.17.24", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "eslint-config-next": "15.2.3", - "next": "^15.2.3", - "next-auth": "5.0.0-beta.25", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "zod": "3.24.2" - } -} diff --git a/playground/nextjs/src/app/(dashboard)/layout.tsx b/playground/nextjs/src/app/(dashboard)/layout.tsx deleted file mode 100644 index 52b3ac44e6b..00000000000 --- a/playground/nextjs/src/app/(dashboard)/layout.tsx +++ /dev/null @@ -1,183 +0,0 @@ -'use client'; -import * as React from 'react'; -import { usePathname, useParams } from 'next/navigation'; -import Typography from '@mui/material/Typography'; -import Stack from '@mui/material/Stack'; -import MenuList from '@mui/material/MenuList'; -import MenuItem from '@mui/material/MenuItem'; -import ListItemText from '@mui/material/ListItemText'; -import ListItemIcon from '@mui/material/ListItemIcon'; -import Avatar from '@mui/material/Avatar'; -import Divider from '@mui/material/Divider'; -import { - Account, - AccountPreview, - AccountPopoverFooter, - SignOutButton, - AccountPreviewProps, -} from '@toolpad/core/Account'; -import { DashboardLayout, SidebarFooterProps } from '@toolpad/core/DashboardLayout'; -import { PageContainer } from '@toolpad/core/PageContainer'; - -const accounts = [ - { - id: 1, - name: 'Bharat Kashyap', - email: 'bharatkashyap@outlook.com', - image: 'https://avatars.githubusercontent.com/u/19550456', - projects: [ - { - id: 3, - title: 'Project X', - }, - ], - }, - { - id: 2, - name: 'Bharat MUI', - email: 'bharat@mui.com', - color: '#8B4513', // Brown color - projects: [{ id: 4, title: 'Project A' }], - }, -]; - -function AccountSidebarPreview(props: AccountPreviewProps & { mini: boolean }) { - const { handleClick, open, mini } = props; - return ( - - - - - ); -} - -function SidebarFooterAccountPopover() { - return ( - - - Accounts - - - {accounts.map((account) => ( - - - - {account.name[0]} - - - - - ))} - - - - - - - ); -} - -const createPreviewComponent = (mini: boolean) => { - function PreviewComponent(props: AccountPreviewProps) { - return ; - } - return PreviewComponent; -}; - -function SidebarFooterAccount({ mini }: SidebarFooterProps) { - const PreviewComponent = React.useMemo(() => createPreviewComponent(mini), [mini]); - return ( - - `drop-shadow(0px 2px 8px ${theme.palette.mode === 'dark' ? 'rgba(255,255,255,0.10)' : 'rgba(0,0,0,0.32)'})`, - mt: 1, - '&::before': { - content: '""', - display: 'block', - position: 'absolute', - bottom: 10, - left: 0, - width: 10, - height: 10, - bgcolor: 'background.paper', - transform: 'translate(-50%, -50%) rotate(45deg)', - zIndex: 0, - }, - }, - }, - }, - }, - }} - /> - ); -} - -export default function DashboardPagesLayout(props: { children: React.ReactNode }) { - const pathname = usePathname(); - const params = useParams(); - const [orderId] = params.segments ?? []; - - const title = React.useMemo(() => { - if (pathname === '/orders/new') { - return 'New Order'; - } - if (orderId && pathname.includes('/edit')) { - return `Order ${orderId} - Edit`; - } - if (orderId) { - return `Order ${orderId}`; - } - return undefined; - }, [orderId, pathname]); - - return ( - null }}> - {props.children} - - ); -} diff --git a/playground/nextjs/src/app/(dashboard)/orders/[[...segments]]/page.tsx b/playground/nextjs/src/app/(dashboard)/orders/[[...segments]]/page.tsx deleted file mode 100644 index feaf48f212e..00000000000 --- a/playground/nextjs/src/app/(dashboard)/orders/[[...segments]]/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import { Crud } from '@toolpad/core/Crud'; -import { ordersDataSource, Order, ordersCache } from '../../../../data/orders'; - -export default function OrdersCrudPage() { - return ( - - dataSource={ordersDataSource} - dataSourceCache={ordersCache} - rootPath="/orders" - initialPageSize={25} - defaultValues={{ itemCount: 1 }} - /> - ); -} diff --git a/playground/nextjs/src/app/(dashboard)/page.tsx b/playground/nextjs/src/app/(dashboard)/page.tsx deleted file mode 100644 index 9fc6ddc433b..00000000000 --- a/playground/nextjs/src/app/(dashboard)/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import * as React from 'react'; -import Typography from '@mui/material/Typography'; -import { auth } from '../../auth'; - -export default async function HomePage() { - const session = await auth(); - - return Welcome to Toolpad, {session?.user?.name || 'User'}!; -} diff --git a/playground/nextjs/src/app/api/auth/[...nextauth]/route.ts b/playground/nextjs/src/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index ca225652075..00000000000 --- a/playground/nextjs/src/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { handlers } from '../../../../auth'; - -export const { GET, POST } = handlers; diff --git a/playground/nextjs/src/app/auth/signin/actions.ts b/playground/nextjs/src/app/auth/signin/actions.ts deleted file mode 100644 index e79fd0b7541..00000000000 --- a/playground/nextjs/src/app/auth/signin/actions.ts +++ /dev/null @@ -1,40 +0,0 @@ -'use server'; -import { AuthError } from 'next-auth'; -import type { AuthProvider } from '@toolpad/core/SignInPage'; -import { signIn as signInAction } from '../../../auth'; - -async function signIn(provider: AuthProvider, formData: FormData, callbackUrl?: string) { - try { - return await signInAction(provider.id, { - ...(formData && { email: formData.get('email'), password: formData.get('password') }), - redirectTo: callbackUrl ?? '/', - }); - } catch (error) { - // The desired flow for successful sign in in all cases - // and unsuccessful sign in for OAuth providers will cause a `redirect`, - // and `redirect` is a throwing function, so we need to re-throw - // to allow the redirect to happen - // Source: https://github.com/vercel/next.js/issues/49298#issuecomment-1542055642 - // Detect a `NEXT_REDIRECT` error and re-throw it - if (error instanceof Error && error.message === 'NEXT_REDIRECT') { - throw error; - } - // Handle Auth.js errors - if (error instanceof AuthError) { - return { - error: - error.type === 'CredentialsSignin' - ? 'Invalid credentials.' - : 'An error with Auth.js occurred.', - type: error.type, - }; - } - // An error boundary must exist to handle unknown errors - return { - error: 'Something went wrong.', - type: 'UnknownError', - }; - } -} - -export default signIn; diff --git a/playground/nextjs/src/app/auth/signin/page.tsx b/playground/nextjs/src/app/auth/signin/page.tsx deleted file mode 100644 index 0347eda917b..00000000000 --- a/playground/nextjs/src/app/auth/signin/page.tsx +++ /dev/null @@ -1,27 +0,0 @@ -'use client'; -import * as React from 'react'; -import Link from '@mui/material/Link'; -import { SignInPage } from '@toolpad/core/SignInPage'; -import { providerMap } from '../../../auth'; -import signIn from './actions'; - -function ForgotPasswordLink() { - return Forgot password?; -} - -function SignUpLink() { - return Sign up; -} - -export default function SignIn() { - return ( - - ); -} diff --git a/playground/nextjs/src/app/layout.tsx b/playground/nextjs/src/app/layout.tsx deleted file mode 100644 index b8d3404a55f..00000000000 --- a/playground/nextjs/src/app/layout.tsx +++ /dev/null @@ -1,57 +0,0 @@ -import * as React from 'react'; -import { NextAppProvider } from '@toolpad/core/nextjs'; -import { AppRouterCacheProvider } from '@mui/material-nextjs/v15-appRouter'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; -import type { Navigation } from '@toolpad/core/AppProvider'; -import { SessionProvider, signIn, signOut } from 'next-auth/react'; -import { auth } from '../auth'; - -const NAVIGATION: Navigation = [ - { - kind: 'header', - title: 'Main items', - }, - { - title: 'Dashboard', - icon: , - }, - { - segment: 'orders', - title: 'Orders', - icon: , - pattern: 'orders{/:orderId}*', - }, -]; - -const BRANDING = { - title: 'My Toolpad Core Next.js App', -}; - -const AUTHENTICATION = { - signIn, - signOut, -}; - -export default async function RootLayout(props: { children: React.ReactNode }) { - const session = await auth(); - - return ( - - - - - - {props.children} - - - - - - ); -} diff --git a/playground/nextjs/src/app/public/layout.tsx b/playground/nextjs/src/app/public/layout.tsx deleted file mode 100644 index 5dee163b753..00000000000 --- a/playground/nextjs/src/app/public/layout.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import * as React from 'react'; -import { DashboardLayout } from '@toolpad/core/DashboardLayout'; -import { PageContainer } from '@toolpad/core/PageContainer'; - -export default async function DashboardPagesLayout(props: { children: React.ReactNode }) { - return ( - - {props.children} - - ); -} diff --git a/playground/nextjs/src/app/public/page.tsx b/playground/nextjs/src/app/public/page.tsx deleted file mode 100644 index 9eb1ae52478..00000000000 --- a/playground/nextjs/src/app/public/page.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from 'react'; -import Typography from '@mui/material/Typography'; - -export default async function HomePage() { - return Public page; -} diff --git a/playground/nextjs/src/auth.ts b/playground/nextjs/src/auth.ts deleted file mode 100644 index 323ac97b646..00000000000 --- a/playground/nextjs/src/auth.ts +++ /dev/null @@ -1,77 +0,0 @@ -import NextAuth from 'next-auth'; -import GitHub from 'next-auth/providers/github'; -import Credentials from 'next-auth/providers/credentials'; -import type { Provider } from 'next-auth/providers'; - -const providers: Provider[] = [ - GitHub({ - clientId: process.env.GITHUB_CLIENT_ID, - clientSecret: process.env.GITHUB_CLIENT_SECRET, - }), - Credentials({ - credentials: { - email: { label: 'Email Address', type: 'email' }, - password: { label: 'Password', type: 'password' }, - }, - authorize(c) { - if (c.password !== 'password') { - return null; - } - return { - id: 'test', - name: 'Test User', - email: String(c.email), - }; - }, - }), -]; - -export const providerMap = providers.map((provider) => { - if (typeof provider === 'function') { - const providerData = provider(); - return { id: providerData.id, name: providerData.name }; - } - return { id: provider.id, name: provider.name }; -}); - -const missingVars: string[] = []; - -if (!process.env.GITHUB_CLIENT_ID) { - missingVars.push('GITHUB_CLIENT_ID'); -} -if (!process.env.GITHUB_CLIENT_SECRET) { - missingVars.push('GITHUB_CLIENT_SECRET'); -} - -if (missingVars.length > 0) { - const baseMessage = - 'Authentication is configured but the following environment variables are missing:'; - - if (process.env.NODE_ENV === 'production') { - throw new Error(`error - ${baseMessage} ${missingVars.join(', ')}`); - } else { - console.warn( - `\u001b[33mwarn\u001b[0m - ${baseMessage} \u001b[31m${missingVars.join(', ')}\u001b[0m`, - ); - } -} - -export const { handlers, auth, signIn, signOut } = NextAuth({ - providers, - secret: process.env.AUTH_SECRET, - pages: { - signIn: '/auth/signin', - }, - callbacks: { - authorized({ auth: session, request: { nextUrl } }) { - const isLoggedIn = !!session?.user; - const isPublicPage = nextUrl.pathname.startsWith('/public'); - - if (isPublicPage || isLoggedIn) { - return true; - } - - return false; // Redirect unauthenticated users to login page - }, - }, -}); diff --git a/playground/nextjs/src/data/orders.ts b/playground/nextjs/src/data/orders.ts deleted file mode 100644 index f0daa1ac650..00000000000 --- a/playground/nextjs/src/data/orders.ts +++ /dev/null @@ -1,199 +0,0 @@ -'use client'; -import { DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud'; -import { z } from 'zod'; - -type OrderStatus = 'Pending' | 'Sent'; - -export interface Order extends DataModel { - id: number; - title: string; - description?: string; - status: OrderStatus; - itemCount: number; - fastDelivery: boolean; - createdAt: string; - deliveryTime?: string; -} - -const getOrdersStore = (): Order[] => { - const value = localStorage.getItem('orders-store'); - return value ? JSON.parse(value) : []; -}; - -const setOrdersStore = (value: Order[]) => { - return localStorage.setItem('orders-store', JSON.stringify(value)); -}; - -export const ordersDataSource: DataSource = { - fields: [ - { field: 'id', headerName: 'ID' }, - { field: 'title', headerName: 'Title' }, - { field: 'description', headerName: 'Description' }, - { - field: 'status', - headerName: 'Status', - type: 'singleSelect', - valueOptions: ['Pending', 'Sent'], - }, - { field: 'itemCount', headerName: 'No. of items', type: 'number' }, - { field: 'fastDelivery', headerName: 'Fast delivery', type: 'boolean' }, - { - field: 'createdAt', - headerName: 'Created at', - type: 'date', - valueGetter: (value: string) => value && new Date(value), - }, - { - field: 'deliveryTime', - headerName: 'Delivery time', - type: 'dateTime', - valueGetter: (value: string) => value && new Date(value), - }, - ], - getMany: async ({ paginationModel, filterModel, sortModel }) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - let filteredOrders = [...ordersStore]; - - // Apply filters (example only) - if (filterModel?.items?.length) { - filterModel.items.forEach(({ field, value, operator }) => { - if (!field || value == null) { - return; - } - - filteredOrders = filteredOrders.filter((order) => { - const orderValue = order[field]; - - switch (operator) { - case 'contains': - return String(orderValue).toLowerCase().includes(String(value).toLowerCase()); - case 'equals': - return orderValue === value; - case 'startsWith': - return String(orderValue).toLowerCase().startsWith(String(value).toLowerCase()); - case 'endsWith': - return String(orderValue).toLowerCase().endsWith(String(value).toLowerCase()); - case '>': - return (orderValue as number) > value; - case '<': - return (orderValue as number) < value; - default: - return true; - } - }); - }); - } - - // Apply sorting - if (sortModel?.length) { - filteredOrders.sort((a, b) => { - for (const { field, sort } of sortModel) { - if ((a[field] as number) < (b[field] as number)) { - return sort === 'asc' ? -1 : 1; - } - if ((a[field] as number) > (b[field] as number)) { - return sort === 'asc' ? 1 : -1; - } - } - return 0; - }); - } - - // Apply pagination - const start = paginationModel.page * paginationModel.pageSize; - const end = start + paginationModel.pageSize; - const paginatedOrders = filteredOrders.slice(start, end); - - return { - items: paginatedOrders, - itemCount: filteredOrders.length, - }; - }, - getOne: async (orderId) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - const orderToShow = ordersStore.find((order) => order.id === Number(orderId)); - - if (!orderToShow) { - throw new Error('Order not found'); - } - return orderToShow; - }, - createOne: async (data) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - const newOrder = { id: ordersStore.length + 1, ...data } as Order; - - setOrdersStore([...ordersStore, newOrder]); - - return newOrder; - }, - updateOne: async (orderId, data) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - let updatedOrder: Order | null = null; - - setOrdersStore( - ordersStore.map((order) => { - if (order.id === Number(orderId)) { - updatedOrder = { ...order, ...data }; - return updatedOrder; - } - return order; - }), - ); - - if (!updatedOrder) { - throw new Error('Order not found'); - } - return updatedOrder; - }, - deleteOne: async (orderId) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - setOrdersStore(ordersStore.filter((order) => order.id !== Number(orderId))); - }, - validate: z.object({ - title: z.string({ required_error: 'Title is required' }).nonempty('Title is required'), - description: z.string().optional(), - status: z.enum(['Pending', 'Sent'], { - errorMap: () => ({ message: 'Status must be "Pending" or "Sent"' }), - }), - itemCount: z - .number({ required_error: 'Item count is required' }) - .min(1, 'Item count must be at least 1'), - fastDelivery: z.boolean({ required_error: 'Fast delivery is required' }), - createdAt: z - .string({ required_error: 'Creation date is required' }) - .nonempty('Creation date is required'), - deliveryTime: z.string().optional(), - })['~standard'].validate, -}; - -export const ordersCache = new DataSourceCache(); diff --git a/playground/nextjs/src/middleware.ts b/playground/nextjs/src/middleware.ts deleted file mode 100644 index 02c48f4db60..00000000000 --- a/playground/nextjs/src/middleware.ts +++ /dev/null @@ -1,6 +0,0 @@ -export { auth as middleware } from './auth'; - -export const config = { - // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher - matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'], -}; diff --git a/playground/nextjs/tsconfig.json b/playground/nextjs/tsconfig.json deleted file mode 100644 index 2cb18e2f53b..00000000000 --- a/playground/nextjs/tsconfig.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "target": "ES2017" - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/playground/vite/.gitignore b/playground/vite/.gitignore deleted file mode 100644 index a547bf36d8d..00000000000 --- a/playground/vite/.gitignore +++ /dev/null @@ -1,24 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? diff --git a/playground/vite/README.md b/playground/vite/README.md deleted file mode 100644 index 77643b462dd..00000000000 --- a/playground/vite/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# React + TypeScript + Vite - -This template provides a minimal setup to get React working in Vite with HMR. - -Currently, two official plugins are available: - -- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh -- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh diff --git a/playground/vite/index.html b/playground/vite/index.html deleted file mode 100644 index 1f790b03947..00000000000 --- a/playground/vite/index.html +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - - - - Toolpad Core Vite - - -
- - - diff --git a/playground/vite/package.json b/playground/vite/package.json deleted file mode 100644 index ed0de67d8e0..00000000000 --- a/playground/vite/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "playground-vite", - "private": true, - "version": "0.13.0", - "type": "module", - "scripts": { - "dev": "vite", - "preview": "vite preview" - }, - "devDependencies": { - "@emotion/react": "11.14.0", - "@emotion/styled": "11.14.0", - "@mui/icons-material": "6.4.7", - "@mui/material": "6.4.7", - "@toolpad/core": "workspace:*", - "@types/react": "^19.0.10", - "@types/react-dom": "^19.0.4", - "@vitejs/plugin-react": "4.3.4", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "react-router": "7.1.5", - "vite": "5.4.14", - "zod": "3.24.2" - } -} diff --git a/playground/vite/public/vite.svg b/playground/vite/public/vite.svg deleted file mode 100644 index e7b8dfb1b2a..00000000000 --- a/playground/vite/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/playground/vite/src/App.tsx b/playground/vite/src/App.tsx deleted file mode 100644 index cc27100e129..00000000000 --- a/playground/vite/src/App.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import DashboardIcon from '@mui/icons-material/Dashboard'; -import ShoppingCartIcon from '@mui/icons-material/ShoppingCart'; -import { Outlet } from 'react-router'; -import { ReactRouterAppProvider } from '@toolpad/core/react-router'; -import type { Navigation } from '@toolpad/core/AppProvider'; - -const NAVIGATION: Navigation = [ - { - kind: 'header', - title: 'Main items', - }, - { - title: 'Dashboard', - icon: , - }, - { - segment: 'orders', - title: 'Orders', - icon: , - pattern: 'orders{/:orderId}*', - }, -]; - -const BRANDING = { - title: 'My Toolpad Core App', -}; - -export default function App() { - return ( - - - - ); -} diff --git a/playground/vite/src/assets/.gitkeep b/playground/vite/src/assets/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/playground/vite/src/data/orders.ts b/playground/vite/src/data/orders.ts deleted file mode 100644 index 761fddc4112..00000000000 --- a/playground/vite/src/data/orders.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { DataModel, DataSource, DataSourceCache } from '@toolpad/core/Crud'; -import { z } from 'zod'; - -type OrderStatus = 'Pending' | 'Sent'; - -export interface Order extends DataModel { - id: number; - title: string; - description?: string; - status: OrderStatus; - itemCount: number; - fastDelivery: boolean; - createdAt: string; - deliveryTime?: string; -} - -const getOrdersStore = (): Order[] => { - const value = localStorage.getItem('orders-store'); - return value ? JSON.parse(value) : []; -}; - -const setOrdersStore = (value: Order[]) => { - return localStorage.setItem('orders-store', JSON.stringify(value)); -}; - -export const ordersDataSource: DataSource = { - fields: [ - { field: 'id', headerName: 'ID' }, - { field: 'title', headerName: 'Title' }, - { field: 'description', headerName: 'Description' }, - { - field: 'status', - headerName: 'Status', - type: 'singleSelect', - valueOptions: ['Pending', 'Sent'], - }, - { field: 'itemCount', headerName: 'No. of items', type: 'number' }, - { field: 'fastDelivery', headerName: 'Fast delivery', type: 'boolean' }, - { - field: 'createdAt', - headerName: 'Created at', - type: 'date', - valueGetter: (value: string) => value && new Date(value), - }, - { - field: 'deliveryTime', - headerName: 'Delivery time', - type: 'dateTime', - valueGetter: (value: string) => value && new Date(value), - }, - ], - getMany: async ({ paginationModel, filterModel, sortModel }) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - let filteredOrders = [...ordersStore]; - - // Apply filters (example only) - if (filterModel?.items?.length) { - filterModel.items.forEach(({ field, value, operator }) => { - if (!field || value == null) { - return; - } - - filteredOrders = filteredOrders.filter((order) => { - const orderValue = order[field]; - - switch (operator) { - case 'contains': - return String(orderValue).toLowerCase().includes(String(value).toLowerCase()); - case 'equals': - return orderValue === value; - case 'startsWith': - return String(orderValue).toLowerCase().startsWith(String(value).toLowerCase()); - case 'endsWith': - return String(orderValue).toLowerCase().endsWith(String(value).toLowerCase()); - case '>': - return (orderValue as number) > value; - case '<': - return (orderValue as number) < value; - default: - return true; - } - }); - }); - } - - // Apply sorting - if (sortModel?.length) { - filteredOrders.sort((a, b) => { - for (const { field, sort } of sortModel) { - if ((a[field] as number) < (b[field] as number)) { - return sort === 'asc' ? -1 : 1; - } - if ((a[field] as number) > (b[field] as number)) { - return sort === 'asc' ? 1 : -1; - } - } - return 0; - }); - } - - // Apply pagination - const start = paginationModel.page * paginationModel.pageSize; - const end = start + paginationModel.pageSize; - const paginatedOrders = filteredOrders.slice(start, end); - - return { - items: paginatedOrders, - itemCount: filteredOrders.length, - }; - }, - getOne: async (orderId) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - const orderToShow = ordersStore.find((order) => order.id === Number(orderId)); - - if (!orderToShow) { - throw new Error('Order not found'); - } - return orderToShow; - }, - createOne: async (data) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - const newOrder = { id: ordersStore.length + 1, ...data } as Order; - - setOrdersStore([...ordersStore, newOrder]); - - return newOrder; - }, - updateOne: async (orderId, data) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - let updatedOrder: Order | null = null; - - setOrdersStore( - ordersStore.map((order) => { - if (order.id === Number(orderId)) { - updatedOrder = { ...order, ...data }; - return updatedOrder; - } - return order; - }), - ); - - if (!updatedOrder) { - throw new Error('Order not found'); - } - return updatedOrder; - }, - deleteOne: async (orderId) => { - // Simulate loading delay - await new Promise((resolve) => { - setTimeout(resolve, 750); - }); - - const ordersStore = getOrdersStore(); - - setOrdersStore(ordersStore.filter((order) => order.id !== Number(orderId))); - }, - validate: z.object({ - title: z.string({ required_error: 'Title is required' }).nonempty('Title is required'), - description: z.string().optional(), - status: z.enum(['Pending', 'Sent'], { - errorMap: () => ({ message: 'Status must be "Pending" or "Sent"' }), - }), - itemCount: z - .number({ required_error: 'Item count is required' }) - .min(1, 'Item count must be at least 1'), - fastDelivery: z.boolean({ required_error: 'Fast delivery is required' }), - createdAt: z - .string({ required_error: 'Creation date is required' }) - .nonempty('Creation date is required'), - deliveryTime: z.string().optional(), - })['~standard'].validate, -}; - -export const ordersCache = new DataSourceCache(); diff --git a/playground/vite/src/layouts/dashboard.tsx b/playground/vite/src/layouts/dashboard.tsx deleted file mode 100644 index 4ae22979981..00000000000 --- a/playground/vite/src/layouts/dashboard.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import * as React from 'react'; -import { Outlet, useLocation, useParams, matchPath } from 'react-router'; -import { DashboardLayout } from '@toolpad/core/DashboardLayout'; -import { PageContainer } from '@toolpad/core/PageContainer'; - -export default function Layout() { - const location = useLocation(); - const { orderId } = useParams(); - - const title = React.useMemo(() => { - if (location.pathname === '/orders/new') { - return 'New Order'; - } - if (matchPath('/orders/:orderId/edit', location.pathname)) { - return `Order ${orderId} - Edit`; - } - if (orderId) { - return `Order ${orderId}`; - } - return undefined; - }, [location.pathname, orderId]); - - return ( - - - - - - ); -} diff --git a/playground/vite/src/main.tsx b/playground/vite/src/main.tsx deleted file mode 100644 index 77ab33b70cd..00000000000 --- a/playground/vite/src/main.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import * as React from 'react'; -import * as ReactDOM from 'react-dom/client'; -import { createBrowserRouter, RouterProvider } from 'react-router'; -import App from './App'; -import Layout from './layouts/dashboard'; -import DashboardPage from './pages'; -import OrdersCrudPage from './pages/orders'; - -const router = createBrowserRouter([ - { - Component: App, - children: [ - { - path: '/', - Component: Layout, - children: [ - { - path: '', - Component: DashboardPage, - }, - { - path: 'orders/:orderId?/*', - Component: OrdersCrudPage, - }, - ], - }, - ], - }, -]); - -ReactDOM.createRoot(document.getElementById('root')!).render( - - - , -); diff --git a/playground/vite/src/pages/index.tsx b/playground/vite/src/pages/index.tsx deleted file mode 100644 index e4581fc26bf..00000000000 --- a/playground/vite/src/pages/index.tsx +++ /dev/null @@ -1,6 +0,0 @@ -import * as React from 'react'; -import Typography from '@mui/material/Typography'; - -export default function DashboardPage() { - return Welcome to Toolpad!; -} diff --git a/playground/vite/src/pages/orders.tsx b/playground/vite/src/pages/orders.tsx deleted file mode 100644 index 40b45f0d306..00000000000 --- a/playground/vite/src/pages/orders.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import * as React from 'react'; -import { Crud } from '@toolpad/core/Crud'; -import { ordersDataSource, Order, ordersCache } from '../data/orders'; - -export default function OrdersCrudPage() { - return ( - - dataSource={ordersDataSource} - dataSourceCache={ordersCache} - rootPath="/orders" - initialPageSize={25} - defaultValues={{ itemCount: 1 }} - /> - ); -} diff --git a/playground/vite/src/vite-env.d.ts b/playground/vite/src/vite-env.d.ts deleted file mode 100644 index 11f02fe2a00..00000000000 --- a/playground/vite/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/playground/vite/tsconfig.json b/playground/vite/tsconfig.json deleted file mode 100644 index 251a83f8a97..00000000000 --- a/playground/vite/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ESNext", - "useDefineForClassFields": true, - "lib": ["DOM", "DOM.Iterable", "ESNext"], - "allowJs": false, - "skipLibCheck": true, - "esModuleInterop": false, - "allowSyntheticDefaultImports": true, - "strict": true, - "forceConsistentCasingInFileNames": true, - "module": "ESNext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "noEmit": true, - "jsx": "react-jsx" - }, - "include": ["src"] -} diff --git a/playground/vite/vite.config.ts b/playground/vite/vite.config.ts deleted file mode 100644 index 627a3196243..00000000000 --- a/playground/vite/vite.config.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 004bb81b79d..63b87fc5b0c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -186,7 +186,7 @@ importers: version: 1.3.2(eslint@8.57.1) eslint-plugin-import: specifier: 2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) + version: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) eslint-plugin-jsx-a11y: specifier: 6.10.2 version: 6.10.2(eslint@8.57.1) @@ -341,8 +341,8 @@ importers: specifier: 7.28.0 version: 7.28.0(@types/react@19.0.10)(react@19.0.0) '@toolpad/core': - specifier: workspace:* - version: link:../packages/toolpad-core/build + specifier: 0.13.0 + version: 0.13.0(bmfgrlon2n5kkedhl54ikegvye) '@toolpad/studio': specifier: workspace:* version: link:../packages/toolpad-studio @@ -537,58 +537,6 @@ importers: specifier: 2.2.1 version: 2.2.1 - packages/create-toolpad-app: - dependencies: - '@inquirer/prompts': - specifier: ^6.0.1 - version: 6.0.1 - '@toolpad/core': - specifier: workspace:* - version: link:../toolpad-core/build - '@toolpad/utils': - specifier: workspace:* - version: link:../toolpad-utils/build - chalk: - specifier: 5.4.1 - version: 5.4.1 - execa: - specifier: 9.5.2 - version: 9.5.2 - invariant: - specifier: 2.2.4 - version: 2.2.4 - semver: - specifier: 7.6.3 - version: 7.6.3 - tar: - specifier: 7.4.3 - version: 7.4.3 - yargs: - specifier: 17.7.2 - version: 17.7.2 - devDependencies: - '@types/inquirer': - specifier: 9.0.7 - version: 9.0.7 - '@types/invariant': - specifier: 2.2.37 - version: 2.2.37 - '@types/node': - specifier: ^20.17.24 - version: 20.17.24 - '@types/semver': - specifier: 7.5.8 - version: 7.5.8 - '@types/tar': - specifier: 6.1.13 - version: 6.1.13 - '@types/yargs': - specifier: 17.0.33 - version: 17.0.33 - terminate: - specifier: ^2.8.0 - version: 2.8.0 - packages/eslint-plugin-material-ui: dependencies: emoji-regex: @@ -602,98 +550,6 @@ importers: specifier: 7.18.0 version: 7.18.0(eslint@8.57.1)(typescript@5.8.2) - packages/toolpad-core: - dependencies: - '@babel/runtime': - specifier: ^7.26.10 - version: 7.26.10 - '@mui/utils': - specifier: 7.0.0-beta.4 - version: 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) - '@standard-schema/spec': - specifier: ^1.0.0 - version: 1.0.0 - '@toolpad/utils': - specifier: workspace:* - version: link:../toolpad-utils/build - '@vitejs/plugin-react': - specifier: 4.3.4 - version: 4.3.4(vite@5.4.14(@types/node@20.17.24)(terser@5.36.0)) - client-only: - specifier: ^0.0.1 - version: 0.0.1 - dayjs: - specifier: 1.11.13 - version: 1.11.13 - invariant: - specifier: 2.2.4 - version: 2.2.4 - path-to-regexp: - specifier: 6.3.0 - version: 6.3.0 - prop-types: - specifier: 15.8.1 - version: 15.8.1 - react: - specifier: ^18 || ^19 - version: 19.0.0 - devDependencies: - '@mui/icons-material': - specifier: 6.4.7 - version: 6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/material': - specifier: 6.4.7 - version: 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/x-data-grid': - specifier: 7.28.0 - version: 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/x-data-grid-premium': - specifier: 7.28.0 - version: 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/x-data-grid-pro': - specifier: 7.28.0 - version: 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/x-date-pickers': - specifier: 7.28.0 - version: 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(date-fns-jalali@2.29.3-0)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@types/invariant': - specifier: 2.2.37 - version: 2.2.37 - '@types/prop-types': - specifier: 15.7.14 - version: 15.7.14 - '@types/react': - specifier: ^19.0.10 - version: 19.0.10 - '@types/react-dom': - specifier: ^19.0.4 - version: 19.0.4(@types/react@19.0.10) - '@types/sinon': - specifier: ^17.0.4 - version: 17.0.4 - '@vitest/browser': - specifier: 2.1.9 - version: 2.1.9(@types/node@20.17.24)(playwright@1.48.2)(typescript@5.8.2)(vite@5.4.14(@types/node@20.17.24)(terser@5.36.0))(vitest@2.1.9) - next: - specifier: ^15.2.3 - version: 15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - next-router-mock: - specifier: ^0.9.13 - version: 0.9.13(next@15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) - playwright: - specifier: ^1.47.2 - version: 1.48.2 - react-router: - specifier: 7.1.5 - version: 7.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - sinon: - specifier: ^19.0.2 - version: 19.0.2 - vitest: - specifier: 2.1.9 - version: 2.1.9(@types/node@20.17.24)(@vitest/browser@2.1.9)(jsdom@25.0.1)(msw@2.6.5(@types/node@20.17.24)(typescript@5.8.2))(terser@5.36.0) - publishDirectory: build - packages/toolpad-studio: dependencies: '@auth/core': @@ -763,8 +619,8 @@ importers: specifier: 5.69.0 version: 5.69.0(@tanstack/react-query@5.69.0(react@19.0.0))(react@19.0.0) '@toolpad/core': - specifier: workspace:* - version: link:../toolpad-core/build + specifier: 0.13.0 + version: 0.13.0(bmfgrlon2n5kkedhl54ikegvye) '@toolpad/studio-components': specifier: workspace:* version: link:../toolpad-studio-components @@ -772,8 +628,8 @@ importers: specifier: workspace:* version: link:../toolpad-studio-runtime '@toolpad/utils': - specifier: workspace:* - version: link:../toolpad-utils/build + specifier: 0.13.0 + version: 0.13.0(react@19.0.0) '@types/cors': specifier: 2.8.17 version: 2.8.17 @@ -1044,7 +900,7 @@ importers: version: 9.1.0(eslint@8.57.1) eslint-plugin-import: specifier: 2.31.0 - version: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) + version: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) formidable: specifier: 3.5.2 version: 3.5.2 @@ -1085,8 +941,8 @@ importers: specifier: workspace:* version: link:../toolpad-studio-runtime '@toolpad/utils': - specifier: workspace:* - version: link:../toolpad-utils/build + specifier: 0.13.0 + version: 0.13.0(react@19.0.0) dayjs: specifier: 1.11.13 version: 1.11.13 @@ -1125,8 +981,8 @@ importers: specifier: 5.69.0 version: 5.69.0(react@19.0.0) '@toolpad/utils': - specifier: workspace:* - version: link:../toolpad-utils/build + specifier: 0.13.0 + version: 0.13.0(react@19.0.0) '@types/json-schema': specifier: 7.0.15 version: 7.0.15 @@ -1174,185 +1030,6 @@ importers: specifier: ^19.0.0 version: 19.0.0 - packages/toolpad-utils: - dependencies: - invariant: - specifier: 2.2.4 - version: 2.2.4 - prettier: - specifier: 3.4.2 - version: 3.4.2 - react: - specifier: ^18.0.0 || ^19.0.0 - version: 19.0.0 - react-is: - specifier: ^19.0.0 - version: 19.0.0 - title: - specifier: 4.0.1 - version: 4.0.1 - yaml: - specifier: 2.5.1 - version: 2.5.1 - yaml-diff-patch: - specifier: 2.0.0 - version: 2.0.0 - devDependencies: - '@types/express': - specifier: 5.0.0 - version: 5.0.0 - '@types/invariant': - specifier: 2.2.37 - version: 2.2.37 - '@types/react': - specifier: ^19.0.10 - version: 19.0.10 - '@types/react-is': - specifier: ^19.0.0 - version: 19.0.0 - '@types/title': - specifier: 3.4.3 - version: 3.4.3 - vitest: - specifier: 2.1.9 - version: 2.1.9(@types/node@20.17.24)(@vitest/browser@2.1.9)(jsdom@25.0.1)(msw@2.6.5(@types/node@20.17.24)(typescript@5.8.2))(terser@5.36.0) - publishDirectory: build - - playground/nextjs: - devDependencies: - '@emotion/react': - specifier: 11.14.0 - version: 11.14.0(@types/react@19.0.10)(react@19.0.0) - '@emotion/styled': - specifier: 11.14.0 - version: 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/icons-material': - specifier: 6.4.7 - version: 6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/material': - specifier: 6.4.7 - version: 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/material-nextjs': - specifier: 6.4.3 - version: 6.4.3(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/server@11.11.0)(@types/react@19.0.10)(next@15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) - '@toolpad/core': - specifier: workspace:* - version: link:../../packages/toolpad-core/build - '@types/node': - specifier: ^20.17.24 - version: 20.17.24 - '@types/react': - specifier: ^19.0.10 - version: 19.0.10 - '@types/react-dom': - specifier: ^19.0.4 - version: 19.0.4(@types/react@19.0.10) - eslint-config-next: - specifier: 15.2.3 - version: 15.2.3(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1)(typescript@5.8.2) - next: - specifier: ^15.2.3 - version: 15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - next-auth: - specifier: 5.0.0-beta.25 - version: 5.0.0-beta.25(next@15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) - react: - specifier: ^19.0.0 - version: 19.0.0 - react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) - zod: - specifier: 3.24.2 - version: 3.24.2 - - playground/nextjs-pages: - devDependencies: - '@emotion/react': - specifier: 11.14.0 - version: 11.14.0(@types/react@19.0.10)(react@19.0.0) - '@emotion/styled': - specifier: 11.14.0 - version: 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/icons-material': - specifier: 6.4.7 - version: 6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/material': - specifier: 6.4.7 - version: 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@mui/material-nextjs': - specifier: 6.4.3 - version: 6.4.3(@emotion/cache@11.14.0)(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/server@11.11.0)(@types/react@19.0.10)(next@15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) - '@toolpad/core': - specifier: workspace:* - version: link:../../packages/toolpad-core/build - '@types/react': - specifier: ^19.0.10 - version: 19.0.10 - '@types/react-dom': - specifier: ^19.0.4 - version: 19.0.4(@types/react@19.0.10) - eslint-config-next: - specifier: 15.2.3 - version: 15.2.3(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1)(typescript@5.8.2) - next: - specifier: ^15.2.3 - version: 15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - next-auth: - specifier: 5.0.0-beta.25 - version: 5.0.0-beta.25(next@15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0) - react: - specifier: ^19.0.0 - version: 19.0.0 - react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) - zod: - specifier: 3.24.2 - version: 3.24.2 - - playground/vite: - devDependencies: - '@emotion/react': - specifier: 11.14.0 - version: 11.14.0(@types/react@19.0.10)(react@19.0.0) - '@emotion/styled': - specifier: 11.14.0 - version: 11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/icons-material': - specifier: 6.4.7 - version: 6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/material': - specifier: 6.4.7 - version: 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - '@toolpad/core': - specifier: workspace:* - version: link:../../packages/toolpad-core/build - '@types/react': - specifier: ^19.0.10 - version: 19.0.10 - '@types/react-dom': - specifier: ^19.0.4 - version: 19.0.4(@types/react@19.0.10) - '@vitejs/plugin-react': - specifier: 4.3.4 - version: 4.3.4(vite@5.4.14(@types/node@20.17.24)(terser@5.36.0)) - react: - specifier: ^19.0.0 - version: 19.0.0 - react-dom: - specifier: ^19.0.0 - version: 19.0.0(react@19.0.0) - react-router: - specifier: 7.1.5 - version: 7.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - vite: - specifier: 5.4.14 - version: 5.4.14(@types/node@20.17.24)(terser@5.36.0) - zod: - specifier: 3.24.2 - version: 3.24.2 - test: devDependencies: '@mui/material': @@ -1362,8 +1039,8 @@ importers: specifier: workspace:* version: link:../packages/toolpad-studio '@toolpad/utils': - specifier: workspace:* - version: link:../packages/toolpad-utils/build + specifier: 0.13.0 + version: 0.13.0(react@19.0.0) '@types/invariant': specifier: 2.2.37 version: 2.2.37 @@ -1479,20 +1156,6 @@ packages: resolution: {integrity: sha512-z2B3EYVhDCDYlQwg55C/P8Xvbupb+XjmRHggIYl1R3yiNEBqy/kT9X/uOVMHxFyhLsM2PXsOOruugJoT11dB7Q==} engines: {node: '>=18.0.0'} - '@auth/core@0.37.2': - resolution: {integrity: sha512-kUvzyvkcd6h1vpeMAojK2y7+PAV5H+0Cc9+ZlKYDFhDY31AlvsB+GW5vNO4qE3Y07KeQgvNO9U0QUx/fN62kBw==} - peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - nodemailer: ^6.8.0 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - '@auth/core@0.38.0': resolution: {integrity: sha512-ClHl44x4cY3wfJmHLpW+XrYqED0fZIzbHmwbExltzroCjR5ts3DLTWzADRba8mJFYZ8JIEJDa+lXnGl0E9Bl7Q==} peerDependencies: @@ -2861,14 +2524,6 @@ packages: cpu: [x64] os: [win32] - '@inquirer/checkbox@3.0.1': - resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==} - engines: {node: '>=18'} - - '@inquirer/confirm@4.0.1': - resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==} - engines: {node: '>=18'} - '@inquirer/confirm@5.0.1': resolution: {integrity: sha512-6ycMm7k7NUApiMGfVc32yIPp28iPKxhGRMqoNDiUjq2RyTAkbs5Fx0TdzBqhabcKvniDdAAvHCmsRjnNfTsogw==} engines: {node: '>=18'} @@ -2879,54 +2534,10 @@ packages: resolution: {integrity: sha512-KKTgjViBQUi3AAssqjUFMnMO3CM3qwCHvePV9EW+zTKGKafFGFF01sc1yOIYjLJ7QU52G/FbzKc+c01WLzXmVQ==} engines: {node: '>=18'} - '@inquirer/core@9.2.1': - resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} - engines: {node: '>=18'} - - '@inquirer/editor@3.0.1': - resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==} - engines: {node: '>=18'} - - '@inquirer/expand@3.0.1': - resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==} - engines: {node: '>=18'} - '@inquirer/figures@1.0.7': resolution: {integrity: sha512-m+Trk77mp54Zma6xLkLuY+mvanPxlE4A7yNKs2HBiyZ4UkVs28Mv5c/pgWrHeInx+USHeX/WEPzjrWrcJiQgjw==} engines: {node: '>=18'} - '@inquirer/input@3.0.1': - resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==} - engines: {node: '>=18'} - - '@inquirer/number@2.0.1': - resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==} - engines: {node: '>=18'} - - '@inquirer/password@3.0.1': - resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==} - engines: {node: '>=18'} - - '@inquirer/prompts@6.0.1': - resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==} - engines: {node: '>=18'} - - '@inquirer/rawlist@3.0.1': - resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==} - engines: {node: '>=18'} - - '@inquirer/search@2.0.1': - resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==} - engines: {node: '>=18'} - - '@inquirer/select@3.0.1': - resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==} - engines: {node: '>=18'} - - '@inquirer/type@2.0.0': - resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} - engines: {node: '>=18'} - '@inquirer/type@3.0.0': resolution: {integrity: sha512-YYykfbw/lefC7yKj7nanzQXILM7r3suIvyFlCcMskc99axmsSewXWkAfXKwMbgxL76iAFVmRwmYdwNZNc8gjog==} engines: {node: '>=18'} @@ -2937,10 +2548,6 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} - '@isaacs/fs-minipass@4.0.1': - resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} - engines: {node: '>=18.0.0'} - '@isaacs/string-locale-compare@1.1.0': resolution: {integrity: sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ==} @@ -3258,8 +2865,8 @@ packages: '@types/react': optional: true - '@mui/utils@7.0.0-beta.4': - resolution: {integrity: sha512-PNFTx/6mc5DCXJgCKfdsRHrf4TXTuJs5tY3njqZJqwe7mFOjCtnscjiCXVXLdBGBRm/ZM66sv6eCtmGZTE0/5w==} + '@mui/utils@7.0.0-beta.3': + resolution: {integrity: sha512-wpyhHEadzRe8gqHACvEqLTiyEMNOBbrNdtPC8kIVPIfFYlwi7F62iC4p2TUpjF/Pu79etWG1K77diKysR/oWYQ==} engines: {node: '>=14.0.0'} peerDependencies: '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -3554,9 +3161,6 @@ packages: '@next/eslint-plugin-next@14.2.25': resolution: {integrity: sha512-L2jcdEEa0bTv1DhE67Cdx1kLLkL0iLL9ILdBYx0j7noi2AUJM7bwcqmcN8awGg+8uyKGAGof/OkFom50x+ZyZg==} - '@next/eslint-plugin-next@15.2.3': - resolution: {integrity: sha512-eNSOIMJtjs+dp4Ms1tB1PPPJUQHP3uZK+OQ7iFY9qXpGO6ojT6imCL+KcUOqE/GXGidWbBZJzYdgAdPHqeCEPA==} - '@next/swc-darwin-arm64@15.2.3': resolution: {integrity: sha512-uaBhA8aLbXLqwjnsHSkxs353WrRgQgiFjduDpc7YXEU0B54IKx3vU+cxQlYwPCyC8uYEEX7THhtQQsfHnvv8dw==} engines: {node: '>= 10'} @@ -3620,10 +3224,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nolyfill/is-core-module@1.0.39': - resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} - engines: {node: '>=12.4.0'} - '@npmcli/agent@2.2.2': resolution: {integrity: sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==} engines: {node: ^16.14.0 || >=18.0.0} @@ -4010,9 +3610,6 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} - '@rushstack/eslint-patch@1.10.4': - resolution: {integrity: sha512-WJgX9nzTqknM393q1QJDJmoW28kUfEnybeTfVNcNAPnIx210RXm2DiXiHzfNPJNIUUb1tJnz/l4QGtJ30PgWmA==} - '@sec-ant/readable-stream@0.4.1': resolution: {integrity: sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==} @@ -4051,18 +3648,6 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} - '@sinonjs/commons@3.0.1': - resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==} - - '@sinonjs/fake-timers@13.0.5': - resolution: {integrity: sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==} - - '@sinonjs/samsam@8.0.2': - resolution: {integrity: sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==} - - '@sinonjs/text-encoding@0.7.3': - resolution: {integrity: sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==} - '@slack/bolt@4.2.1': resolution: {integrity: sha512-O+c7i5iZKlxt6ltJAu2BclEoyWuAVkcpir1F3HWCHTez8Pjz0GxwdBzNHR5HDXvOdBT7En1BU0T2L6Ldv++GSg==} engines: {node: '>=18', npm: '>=8.6.0'} @@ -4144,6 +3729,30 @@ packages: peerDependencies: '@testing-library/dom': '>=7.21.4' + '@toolpad/core@0.13.0': + resolution: {integrity: sha512-fnl8sG8XGq+16cQ1uspNkCsVtwtC0979TDCCIJPUXyPGCDU9m8d2QyxmQL6MNqFT1KnfLZkmsFnknht6qlxy0Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/icons-material': ^6.4.0 || ^7.0.0 || ^7.0.0-beta + '@mui/material': ^6.4.0 || ^7.0.0 || ^7.0.0-beta + '@mui/x-data-grid': ^7 + '@mui/x-data-grid-premium': ^7 + '@mui/x-data-grid-pro': ^7 + '@mui/x-date-pickers': ^7 + next: ^14 || ^15 + react: ^18 || ^19 + react-router: ^7 + peerDependenciesMeta: + next: + optional: true + react-router: + optional: true + + '@toolpad/utils@0.13.0': + resolution: {integrity: sha512-8zvJWFOkcv6bMXPOczB47DigNu+CqOgLAu7pUv69+1mGVXMO/Gzk2TQhSHTL71eVRChrE1MKQ/PPZsowPHxFYg==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + '@tootallnate/once@2.0.0': resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==} engines: {node: '>= 10'} @@ -4287,9 +3896,6 @@ packages: '@types/http-proxy@1.17.15': resolution: {integrity: sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==} - '@types/inquirer@9.0.7': - resolution: {integrity: sha512-Q0zyBupO6NxGRZut/JdmqYKOnN95Eg5V8Csg3PGKkP+FnvsUZx1jAyK7fztIszxxMuoBA6E3KXWvdZVXIpx60g==} - '@types/invariant@2.2.37': resolution: {integrity: sha512-IwpIMieE55oGWiXkQPSBY1nw1nFs6bsKXTFskNY8sdS17K24vyEBRQZEwlRS7ZmXCWnJcQtbxWzly+cODWGs2A==} @@ -4335,9 +3941,6 @@ packages: '@types/ms@0.7.34': resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - '@types/mute-stream@0.0.4': - resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} - '@types/node-fetch@2.6.12': resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==} @@ -4405,12 +4008,6 @@ packages: '@types/serve-static@1.15.7': resolution: {integrity: sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==} - '@types/sinon@17.0.4': - resolution: {integrity: sha512-RHnIrhfPO3+tJT0s7cFaXGZvsL4bbR3/k7z3P312qMS4JaS2Tk+KiwiLx1S0rQ56ERj00u1/BtdyVd0FY+Pdew==} - - '@types/sinonjs__fake-timers@8.1.5': - resolution: {integrity: sha512-mQkU2jY8jJEF7YHjHvsQO8+3ughTL1mcnn96igfhONmR+fUPSKIkefQYpSe8bsly2Ep7oQbn/6VG5/9/0qcArQ==} - '@types/source-list-map@0.1.6': resolution: {integrity: sha512-5JcVt1u5HDmlXkwOD2nslZVllBBc7HDuOICfiZah2Z0is8M8g+ddAEawbmd3VjedfDHBzxCaXLs07QEmb7y54g==} @@ -4423,15 +4020,6 @@ packages: '@types/tapable@1.0.12': resolution: {integrity: sha512-bTHG8fcxEqv1M9+TD14P8ok8hjxoOCkfKc8XXLaaD05kI7ohpeI956jtDOD3XHKBQrlyPughUtzm1jtVhHpA5Q==} - '@types/tar@6.1.13': - resolution: {integrity: sha512-IznnlmU5f4WcGTh2ltRu/Ijpmk8wiWXfF0VA4s+HPjHZgvFggk1YaIkbo5krX/zUCzWF8N/l4+W/LNxnvAJ8nw==} - - '@types/through@0.0.33': - resolution: {integrity: sha512-HsJ+z3QuETzP3cswwtzt2vEIiHBk/dCcHGhbmG5X3ecnwFD/lPrMpliGXxSCg03L9AhrdwA4Oz/qfspkDW+xGQ==} - - '@types/title@3.4.3': - resolution: {integrity: sha512-mjupLOb4kwUuoUFokkacy/VMRVBH2qtqZ5AX7K7iha6+iKIkX80n/Y4EoNVEVRmer8dYJU/ry+fppUaDFVQh7Q==} - '@types/tough-cookie@4.0.5': resolution: {integrity: sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==} @@ -4459,9 +4047,6 @@ packages: '@types/whatwg-url@11.0.5': resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==} - '@types/wrap-ansi@3.0.0': - resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} - '@types/ws@8.18.0': resolution: {integrity: sha512-8svvI3hMyvN0kKCJMvTJP/x6Y/EoQbepff882wL+Sn5QsXb3etnamgrJq4isrBxSJj5L2AuXcI0+bgkoAXGUJw==} @@ -5257,10 +4842,6 @@ packages: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} - chownr@3.0.0: - resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} - engines: {node: '>=18'} - chrome-trace-event@1.0.4: resolution: {integrity: sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==} engines: {node: '>=6.0'} @@ -5874,10 +5455,6 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - diff@7.0.0: - resolution: {integrity: sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==} - engines: {node: '>=0.3.1'} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -6096,15 +5673,6 @@ packages: eslint-plugin-react: ^7.28.0 eslint-plugin-react-hooks: ^4.3.0 - eslint-config-next@15.2.3: - resolution: {integrity: sha512-VDQwbajhNMFmrhLWVyUXCqsGPN+zz5G8Ys/QwFubfsxTIrkqdx3N3x3QPW+pERz8bzGPP0IgEm8cNbZcd8PFRQ==} - peerDependencies: - eslint: ^7.23.0 || ^8.0.0 || ^9.0.0 - typescript: '>=3.3.1' - peerDependenciesMeta: - typescript: - optional: true - eslint-config-prettier@9.1.0: resolution: {integrity: sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==} hasBin: true @@ -6120,19 +5688,6 @@ packages: eslint-import-resolver-node@0.3.9: resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==} - eslint-import-resolver-typescript@3.6.3: - resolution: {integrity: sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - eslint: '*' - eslint-plugin-import: '*' - eslint-plugin-import-x: '*' - peerDependenciesMeta: - eslint-plugin-import: - optional: true - eslint-plugin-import-x: - optional: true - eslint-import-resolver-webpack@0.13.10: resolution: {integrity: sha512-ciVTEg7sA56wRMR772PyjcBRmyBMLS46xgzQZqt6cWBEKc7cK65ZSSLCTLVRu2gGtKyXUb5stwf4xxLBfERLFA==} engines: {node: '>= 6'} @@ -6287,9 +5842,6 @@ packages: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} - event-stream@3.3.4: - resolution: {integrity: sha512-QHpkERcGsR0T7Qm3HNJSyXKEEj8AHNxkY3PK8TS2KJvQ7NiSHe3DDpwVKKtoYprL/AreyzFBeIkBIWChAqn60g==} - event-target-shim@5.0.1: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} @@ -6364,10 +5916,6 @@ packages: fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-glob@3.3.1: - resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} - engines: {node: '>=8.6.0'} - fast-glob@3.3.3: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} @@ -6538,9 +6086,6 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} - from@0.1.7: - resolution: {integrity: sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==} - front-matter@4.0.2: resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} @@ -7069,9 +6614,6 @@ packages: resolution: {integrity: sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==} engines: {node: '>= 0.4'} - is-bun-module@1.2.1: - resolution: {integrity: sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==} - is-callable@1.2.7: resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} engines: {node: '>= 0.4'} @@ -7357,9 +6899,6 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} - jose@5.9.6: - resolution: {integrity: sha512-AMlnetc9+CV9asI19zHmrgS/WYsWUwCn2R7RzlbJWD7F9eWYUTGyBmU9o6PxngtLGOiDGPRu+Uc4fhKzbpteZQ==} - jose@6.0.8: resolution: {integrity: sha512-EyUPtOKyTYq+iMOszO42eobQllaIjJnwkZ2U93aJzNyPibCy7CEvT9UQnaCVB51IAd49gbNdCew1c0LcLTCB2g==} @@ -7505,9 +7044,6 @@ packages: just-diff@6.0.2: resolution: {integrity: sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA==} - just-extend@6.2.0: - resolution: {integrity: sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==} - jwa@1.4.1: resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} @@ -7665,10 +7201,6 @@ packages: lodash.flatten@4.4.0: resolution: {integrity: sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==} - lodash.get@4.4.2: - resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} - deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. - lodash.groupby@4.6.0: resolution: {integrity: sha512-5dcWxm23+VAoz+awKmBaiBvzox8+RqMgFhi7UvX9DHZr2HdxHXM/Wrf8cfKpsW37RNrvtPn6hSwNqurSILbmJw==} @@ -7822,9 +7354,6 @@ packages: resolution: {integrity: sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==} engines: {node: '>=8'} - map-stream@0.1.0: - resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==} - markdown-it@14.1.0: resolution: {integrity: sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==} hasBin: true @@ -8113,21 +7642,12 @@ packages: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} - minizlib@3.0.1: - resolution: {integrity: sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==} - engines: {node: '>= 18'} - mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true mkdirp@1.0.4: - resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} - engines: {node: '>=10'} - hasBin: true - - mkdirp@3.0.1: - resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} hasBin: true @@ -8228,28 +7748,6 @@ packages: nested-error-stacks@2.1.1: resolution: {integrity: sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==} - next-auth@5.0.0-beta.25: - resolution: {integrity: sha512-2dJJw1sHQl2qxCrRk+KTQbeH+izFbGFPuJj5eGgBZFYyiYYtvlrBeUw1E/OJJxTRjuxbSYGnCTkUIRsIIW0bog==} - peerDependencies: - '@simplewebauthn/browser': ^9.0.1 - '@simplewebauthn/server': ^9.0.2 - next: ^14.0.0-0 || ^15.0.0-0 - nodemailer: ^6.6.5 - react: ^18.2.0 || ^19.0.0-0 - peerDependenciesMeta: - '@simplewebauthn/browser': - optional: true - '@simplewebauthn/server': - optional: true - nodemailer: - optional: true - - next-router-mock@0.9.13: - resolution: {integrity: sha512-906n2RRaE6Y28PfYJbaz5XZeJ6Tw8Xz1S6E31GGwZ0sXB6/XjldD1/2azn1ZmBmRk5PQRkzjg+n+RHZe5xQzWA==} - peerDependencies: - next: '>=10.0.0' - react: '>=17.0.0' - next@15.2.3: resolution: {integrity: sha512-x6eDkZxk2rPpu46E1ZVUWIBhYCLszmUY6fvHBFcbzJ9dD+qRX6vcHusaqqDlnY+VngKzKbAiG2iRCkPbmi8f7w==} engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0} @@ -8271,9 +7769,6 @@ packages: sass: optional: true - nise@6.1.1: - resolution: {integrity: sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==} - node-cleanup@2.1.2: resolution: {integrity: sha512-qN8v/s2PAJwGUtr1/hYTpNKlD6Y9rc4p8KSmJXyGdYGZsDGKXrGThikLFP9OCHFeLeEpQzPwiAtdIvBLqm//Hw==} @@ -8755,9 +8250,6 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} - pause-stream@0.0.11: - resolution: {integrity: sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==} - perf-cascade@3.0.3: resolution: {integrity: sha512-2TFyFcBdJrbVU5DGlq32yVimwZ3FQkBX5zB+g2/ps/E60RrcUEUvuHgrPxpoUulhcujejGtIGlp9muQ+WU6Tlw==} @@ -8947,19 +8439,11 @@ packages: postgres-range@1.1.4: resolution: {integrity: sha512-i/hbxIE9803Alj/6ytL7UHQxRvZkI9O4Sy+J3HGc4F4oo/2eQAjTSNJ0bfxyse3bH0nuVesCk+3IRLaMtG3H6w==} - preact-render-to-string@5.2.3: - resolution: {integrity: sha512-aPDxUn5o3GhWdtJtW0svRC2SS/l8D9MAgo2+AWml+BhDImb27ALf04Q2d+AHqUUOc6RdSXFIBVa2gxzgMKgtZA==} - peerDependencies: - preact: '>=10' - preact-render-to-string@6.5.11: resolution: {integrity: sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==} peerDependencies: preact: '>=10' - preact@10.11.3: - resolution: {integrity: sha512-eY93IVpod/zG3uMF22Unl8h9KkrcKIRs2EGar8hwLZZDU1lkjph303V9HZBwufh2s736U6VXuhD109LYqPoffg==} - preact@10.24.3: resolution: {integrity: sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==} @@ -8989,9 +8473,6 @@ packages: resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - pretty-format@3.8.0: - resolution: {integrity: sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==} - pretty-ms@9.1.0: resolution: {integrity: sha512-o1piW0n3tgKIKCwk2vpM/vOV13zjJzvP37Ioze54YlTHE06m4tjEbzg9WsKkvTuyYln2DHjo5pY4qrZGI0otpw==} engines: {node: '>=18'} @@ -9068,11 +8549,6 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - ps-tree@1.2.0: - resolution: {integrity: sha512-0VnamPPYHl4uaU/nSFeZZpR21QAWRz+sRv4iW9+v/GS/J5U5iZB5BNN6J0RMoOvdx2gWM2+ZFMIm58q24e4UYA==} - engines: {node: '>= 0.10'} - hasBin: true - psl@1.9.0: resolution: {integrity: sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==} @@ -9462,10 +8938,6 @@ packages: engines: {node: '>=14'} hasBin: true - rimraf@5.0.10: - resolution: {integrity: sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==} - hasBin: true - rimraf@6.0.1: resolution: {integrity: sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==} engines: {node: 20 || >=22} @@ -9655,9 +9127,6 @@ packages: simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} - sinon@19.0.2: - resolution: {integrity: sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==} - sirv@2.0.4: resolution: {integrity: sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==} engines: {node: '>= 10'} @@ -9743,9 +9212,6 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} - split@0.3.3: - resolution: {integrity: sha512-wD2AeVmxXRBoX44wAycgjVpMhvbwdI2aZjCkvfNcH1YqHQvJVa1duWc73OyVGJUc05fhFaTZeQ/PYsrmyH0JVA==} - split@1.0.1: resolution: {integrity: sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==} @@ -9773,9 +9239,6 @@ packages: std-env@3.8.0: resolution: {integrity: sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w==} - stream-combiner@0.0.4: - resolution: {integrity: sha512-rT00SPnTVyRsaSz5zgSPma/aHSOic5U1prhYdRy5HS2kTZviFpmDgzilbtsJsxiroqACmayynDN/9VzIbX5DOw==} - streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -9989,18 +9452,10 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} - tar@7.4.3: - resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} - engines: {node: '>=18'} - temp-dir@1.0.0: resolution: {integrity: sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==} engines: {node: '>=4'} - terminate@2.8.0: - resolution: {integrity: sha512-bcbjJEg0wY5nuJXvGxxHfmoEPkyHLCctUKO6suwtxy7jVSgGcgPeGwpbLDLELFhIaxCGRr3dPvyNg1yuz2V0eg==} - engines: {node: '>=12'} - terser-webpack-plugin@5.3.11: resolution: {integrity: sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==} engines: {node: '>= 10.13.0'} @@ -10227,14 +9682,6 @@ packages: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - type-detect@4.0.8: - resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} - engines: {node: '>=4'} - - type-detect@4.1.0: - resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==} - engines: {node: '>=4'} - type-fest@0.18.1: resolution: {integrity: sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==} engines: {node: '>=10'} @@ -10734,10 +10181,6 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yallist@5.0.0: - resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} - engines: {node: '>=18'} - yaml-diff-patch@2.0.0: resolution: {integrity: sha512-RhfIQPGcKSZhsUmsczXAeg5jNhWXk3tAmhl2kjfZthdyaL0XXXOpvRozUp22HvPStmZsHu8T30/UEfX9oIwGxw==} engines: {node: '>=14'} @@ -10806,9 +10249,6 @@ packages: zod@3.23.8: resolution: {integrity: sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==} - zod@3.24.2: - resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==} - zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} @@ -10948,16 +10388,6 @@ snapshots: '@argos-ci/util@2.2.1': {} - '@auth/core@0.37.2': - dependencies: - '@panva/hkdf': 1.2.1 - '@types/cookie': 0.6.0 - cookie: 0.7.1 - jose: 5.9.6 - oauth4webapi: 3.3.0 - preact: 10.11.3 - preact-render-to-string: 5.2.3(preact@10.11.3) - '@auth/core@0.38.0': dependencies: '@panva/hkdf': 1.2.1 @@ -12314,19 +11744,6 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true - '@inquirer/checkbox@3.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/figures': 1.0.7 - '@inquirer/type': 2.0.0 - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 - - '@inquirer/confirm@4.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 2.0.0 - '@inquirer/confirm@5.0.1(@types/node@20.17.24)': dependencies: '@inquirer/core': 10.0.1(@types/node@20.17.24) @@ -12347,89 +11764,8 @@ snapshots: transitivePeerDependencies: - '@types/node' - '@inquirer/core@9.2.1': - dependencies: - '@inquirer/figures': 1.0.7 - '@inquirer/type': 2.0.0 - '@types/mute-stream': 0.0.4 - '@types/node': 20.17.24 - '@types/wrap-ansi': 3.0.0 - ansi-escapes: 4.3.2 - cli-width: 4.1.0 - mute-stream: 1.0.0 - signal-exit: 4.1.0 - strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 - yoctocolors-cjs: 2.1.2 - - '@inquirer/editor@3.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 2.0.0 - external-editor: 3.1.0 - - '@inquirer/expand@3.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 2.0.0 - yoctocolors-cjs: 2.1.2 - '@inquirer/figures@1.0.7': {} - '@inquirer/input@3.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 2.0.0 - - '@inquirer/number@2.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 2.0.0 - - '@inquirer/password@3.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 2.0.0 - ansi-escapes: 4.3.2 - - '@inquirer/prompts@6.0.1': - dependencies: - '@inquirer/checkbox': 3.0.1 - '@inquirer/confirm': 4.0.1 - '@inquirer/editor': 3.0.1 - '@inquirer/expand': 3.0.1 - '@inquirer/input': 3.0.1 - '@inquirer/number': 2.0.1 - '@inquirer/password': 3.0.1 - '@inquirer/rawlist': 3.0.1 - '@inquirer/search': 2.0.1 - '@inquirer/select': 3.0.1 - - '@inquirer/rawlist@3.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/type': 2.0.0 - yoctocolors-cjs: 2.1.2 - - '@inquirer/search@2.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/figures': 1.0.7 - '@inquirer/type': 2.0.0 - yoctocolors-cjs: 2.1.2 - - '@inquirer/select@3.0.1': - dependencies: - '@inquirer/core': 9.2.1 - '@inquirer/figures': 1.0.7 - '@inquirer/type': 2.0.0 - ansi-escapes: 4.3.2 - yoctocolors-cjs: 2.1.2 - - '@inquirer/type@2.0.0': - dependencies: - mute-stream: 1.0.0 - '@inquirer/type@3.0.0(@types/node@20.17.24)': dependencies: '@types/node': 20.17.24 @@ -12443,10 +11779,6 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 - '@isaacs/fs-minipass@4.0.1': - dependencies: - minipass: 7.1.2 - '@isaacs/string-locale-compare@1.1.0': {} '@istanbuljs/schema@0.1.3': {} @@ -12873,7 +12205,7 @@ snapshots: optionalDependencies: '@types/react': 19.0.10 - '@mui/utils@7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0)': + '@mui/utils@7.0.0-beta.3(@types/react@19.0.10)(react@19.0.0)': dependencies: '@babel/runtime': 7.26.10 '@mui/types': 7.3.0(@types/react@19.0.10) @@ -12908,7 +12240,7 @@ snapshots: '@babel/runtime': 7.26.10 '@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/system': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/utils': 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) + '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) '@mui/x-charts-vendor': 7.20.0 '@mui/x-internals': 7.28.0(@types/react@19.0.10)(react@19.0.0) '@react-spring/rafz': 9.7.5 @@ -12928,7 +12260,7 @@ snapshots: '@babel/runtime': 7.26.10 '@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/system': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/utils': 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) + '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) '@mui/x-data-grid': 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/x-data-grid-pro': 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/x-internals': 7.28.0(@types/react@19.0.10)(react@19.0.0) @@ -12951,7 +12283,7 @@ snapshots: '@babel/runtime': 7.26.10 '@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/system': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/utils': 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) + '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) '@mui/x-data-grid': 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/x-internals': 7.28.0(@types/react@19.0.10)(react@19.0.0) '@mui/x-license': 7.28.0(@types/react@19.0.10)(react@19.0.0) @@ -12972,7 +12304,7 @@ snapshots: '@babel/runtime': 7.26.10 '@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/system': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/utils': 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) + '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) '@mui/x-internals': 7.28.0(@types/react@19.0.10)(react@19.0.0) clsx: 2.1.1 prop-types: 15.8.1 @@ -12991,7 +12323,7 @@ snapshots: '@babel/runtime': 7.26.10 '@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/system': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/utils': 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) + '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) '@mui/x-date-pickers': 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(date-fns-jalali@2.29.3-0)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/x-internals': 7.28.0(@types/react@19.0.10)(react@19.0.0) '@mui/x-license': 7.28.0(@types/react@19.0.10)(react@19.0.0) @@ -13013,7 +12345,7 @@ snapshots: '@babel/runtime': 7.26.10 '@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/system': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/utils': 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) + '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) '@mui/x-internals': 7.28.0(@types/react@19.0.10)(react@19.0.0) '@types/react-transition-group': 4.4.12(@types/react@19.0.10) clsx: 2.1.1 @@ -13032,7 +12364,7 @@ snapshots: '@mui/x-internals@7.28.0(@types/react@19.0.10)(react@19.0.0)': dependencies: '@babel/runtime': 7.26.10 - '@mui/utils': 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) + '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 transitivePeerDependencies: - '@types/react' @@ -13040,7 +12372,7 @@ snapshots: '@mui/x-license@7.28.0(@types/react@19.0.10)(react@19.0.0)': dependencies: '@babel/runtime': 7.26.10 - '@mui/utils': 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) + '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) '@mui/x-internals': 7.28.0(@types/react@19.0.10)(react@19.0.0) react: 19.0.0 transitivePeerDependencies: @@ -13051,7 +12383,7 @@ snapshots: '@babel/runtime': 7.26.10 '@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@mui/system': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) - '@mui/utils': 7.0.0-beta.4(@types/react@19.0.10)(react@19.0.0) + '@mui/utils': 6.4.6(@types/react@19.0.10)(react@19.0.0) '@mui/x-internals': 7.28.0(@types/react@19.0.10)(react@19.0.0) '@types/react-transition-group': 4.4.12(@types/react@19.0.10) clsx: 2.1.1 @@ -13151,10 +12483,6 @@ snapshots: dependencies: glob: 10.3.10 - '@next/eslint-plugin-next@15.2.3': - dependencies: - fast-glob: 3.3.1 - '@next/swc-darwin-arm64@15.2.3': optional: true @@ -13194,8 +12522,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@nolyfill/is-core-module@1.0.39': {} - '@npmcli/agent@2.2.2': dependencies: agent-base: 7.1.1 @@ -13648,8 +12974,6 @@ snapshots: '@rtsao/scc@1.1.0': {} - '@rushstack/eslint-patch@1.10.4': {} - '@sec-ant/readable-stream@0.4.1': {} '@sigstore/bundle@2.3.2': @@ -13690,22 +13014,6 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} - '@sinonjs/commons@3.0.1': - dependencies: - type-detect: 4.0.8 - - '@sinonjs/fake-timers@13.0.5': - dependencies: - '@sinonjs/commons': 3.0.1 - - '@sinonjs/samsam@8.0.2': - dependencies: - '@sinonjs/commons': 3.0.1 - lodash.get: 4.4.2 - type-detect: 4.1.0 - - '@sinonjs/text-encoding@0.7.3': {} - '@slack/bolt@4.2.1(@types/express@5.0.0)': dependencies: '@slack/logger': 4.0.0 @@ -13830,6 +13138,43 @@ snapshots: dependencies: '@testing-library/dom': 10.4.0 + '@toolpad/core@0.13.0(bmfgrlon2n5kkedhl54ikegvye)': + dependencies: + '@babel/runtime': 7.26.10 + '@mui/icons-material': 6.4.7(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@types/react@19.0.10)(react@19.0.0) + '@mui/material': 6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/utils': 7.0.0-beta.3(@types/react@19.0.10)(react@19.0.0) + '@mui/x-data-grid': 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/x-data-grid-premium': 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/x-data-grid-pro': 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@mui/x-date-pickers': 7.28.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@mui/material@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(@mui/system@6.4.7(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@emotion/styled@11.14.0(@emotion/react@11.14.0(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(react@19.0.0))(@types/react@19.0.10)(date-fns-jalali@2.29.3-0)(dayjs@1.11.13)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@standard-schema/spec': 1.0.0 + '@toolpad/utils': 0.13.0(react@19.0.0) + '@vitejs/plugin-react': 4.3.4(vite@5.4.14(@types/node@20.17.24)(terser@5.36.0)) + client-only: 0.0.1 + dayjs: 1.11.13 + invariant: 2.2.4 + path-to-regexp: 6.3.0 + prop-types: 15.8.1 + react: 19.0.0 + optionalDependencies: + next: 15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + react-router: 7.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + transitivePeerDependencies: + - '@types/react' + - supports-color + - vite + + '@toolpad/utils@0.13.0(react@19.0.0)': + dependencies: + invariant: 2.2.4 + prettier: 3.4.2 + react: 19.0.0 + react-is: 19.0.0 + title: 4.0.1 + yaml: 2.5.1 + yaml-diff-patch: 2.0.0 + '@tootallnate/once@2.0.0': {} '@trendmicro/react-interpolate@0.5.5(react@19.0.0)': @@ -14003,11 +13348,6 @@ snapshots: dependencies: '@types/node': 20.17.24 - '@types/inquirer@9.0.7': - dependencies: - '@types/through': 0.0.33 - rxjs: 7.8.1 - '@types/invariant@2.2.37': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -14046,10 +13386,6 @@ snapshots: '@types/ms@0.7.34': {} - '@types/mute-stream@0.0.4': - dependencies: - '@types/node': 20.17.24 - '@types/node-fetch@2.6.12': dependencies: '@types/node': 20.17.24 @@ -14129,12 +13465,6 @@ snapshots: '@types/node': 20.17.24 '@types/send': 0.17.4 - '@types/sinon@17.0.4': - dependencies: - '@types/sinonjs__fake-timers': 8.1.5 - - '@types/sinonjs__fake-timers@8.1.5': {} - '@types/source-list-map@0.1.6': {} '@types/statuses@2.0.5': {} @@ -14143,17 +13473,6 @@ snapshots: '@types/tapable@1.0.12': {} - '@types/tar@6.1.13': - dependencies: - '@types/node': 20.17.24 - minipass: 4.2.8 - - '@types/through@0.0.33': - dependencies: - '@types/node': 20.17.24 - - '@types/title@3.4.3': {} - '@types/tough-cookie@4.0.5': {} '@types/uglify-js@3.17.5': @@ -14195,8 +13514,6 @@ snapshots: dependencies: '@types/webidl-conversions': 7.0.3 - '@types/wrap-ansi@3.0.0': {} - '@types/ws@8.18.0': dependencies: '@types/node': 20.17.24 @@ -15232,8 +14549,6 @@ snapshots: chownr@2.0.0: {} - chownr@3.0.0: {} - chrome-trace-event@1.0.4: {} ci-info@3.9.0: {} @@ -15847,8 +15162,6 @@ snapshots: diff-sequences@29.6.3: {} - diff@7.0.0: {} - dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -16168,7 +15481,7 @@ snapshots: dependencies: confusing-browser-globals: 1.0.11 eslint: 8.57.1 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) object.assign: 4.1.7 object.entries: 1.1.8 semver: 6.3.1 @@ -16177,33 +15490,13 @@ snapshots: dependencies: eslint: 8.57.1 eslint-config-airbnb-base: 15.0.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) eslint-plugin-react: 7.37.4(eslint@8.57.1) eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) object.assign: 4.1.7 object.entries: 1.1.8 - eslint-config-next@15.2.3(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1)(typescript@5.8.2): - dependencies: - '@next/eslint-plugin-next': 15.2.3 - '@rushstack/eslint-patch': 1.10.4 - '@typescript-eslint/eslint-plugin': 8.26.1(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint@8.57.1)(typescript@5.8.2) - '@typescript-eslint/parser': 8.26.1(eslint@8.57.1)(typescript@5.8.2) - eslint: 8.57.1 - eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint-plugin-import@2.31.0)(eslint@8.57.1) - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) - eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) - eslint-plugin-react: 7.37.4(eslint@8.57.1) - eslint-plugin-react-hooks: 5.2.0(eslint@8.57.1) - optionalDependencies: - typescript: 5.8.2 - transitivePeerDependencies: - - eslint-import-resolver-webpack - - eslint-plugin-import-x - - supports-color - eslint-config-prettier@9.1.0(eslint@8.57.1): dependencies: eslint: 8.57.1 @@ -16211,7 +15504,7 @@ snapshots: eslint-import-resolver-exports@1.0.0-beta.5(eslint-plugin-import@2.31.0)(eslint@8.57.1): dependencies: eslint: 8.57.1 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) resolve.exports: 2.0.2 eslint-import-resolver-node@0.3.9: @@ -16222,30 +15515,11 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-import-resolver-typescript@3.6.3(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint-plugin-import@2.31.0)(eslint@8.57.1): - dependencies: - '@nolyfill/is-core-module': 1.0.39 - debug: 4.3.7 - enhanced-resolve: 5.17.1 - eslint: 8.57.1 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) - fast-glob: 3.3.3 - get-tsconfig: 4.8.1 - is-bun-module: 1.2.1 - is-glob: 4.0.3 - optionalDependencies: - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) - transitivePeerDependencies: - - '@typescript-eslint/parser' - - eslint-import-resolver-node - - eslint-import-resolver-webpack - - supports-color - eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)): dependencies: debug: 3.2.7 enhanced-resolve: 0.9.1 - eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) + eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) find-root: 1.1.0 hasown: 2.0.2 interpret: 1.4.0 @@ -16258,14 +15532,13 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1): + eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1): dependencies: debug: 3.2.7 optionalDependencies: '@typescript-eslint/parser': 8.26.1(eslint@8.57.1)(typescript@5.8.2) eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-import-resolver-typescript: 3.6.3(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-import-resolver-webpack: 0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)) transitivePeerDependencies: - supports-color @@ -16278,7 +15551,7 @@ snapshots: lodash.snakecase: 4.1.1 lodash.upperfirst: 4.3.1 - eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1): + eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1): dependencies: '@rtsao/scc': 1.1.0 array-includes: 3.1.8 @@ -16289,7 +15562,7 @@ snapshots: doctrine: 2.1.0 eslint: 8.57.1 eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.3)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) + eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.1(eslint@8.57.1)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-webpack@0.13.10(eslint-plugin-import@2.31.0)(webpack@5.98.0(esbuild@0.24.2)))(eslint@8.57.1) hasown: 2.0.2 is-core-module: 2.16.0 is-glob: 4.0.3 @@ -16340,8 +15613,8 @@ snapshots: '@babel/plugin-proposal-private-methods': 7.18.6(@babel/core@7.26.10) eslint: 8.57.1 hermes-parser: 0.25.1 - zod: 3.24.2 - zod-validation-error: 3.4.0(zod@3.24.2) + zod: 3.23.8 + zod-validation-error: 3.4.0(zod@3.23.8) transitivePeerDependencies: - supports-color @@ -16487,16 +15760,6 @@ snapshots: etag@1.8.1: {} - event-stream@3.3.4: - dependencies: - duplexer: 0.1.2 - from: 0.1.7 - map-stream: 0.1.0 - pause-stream: 0.0.11 - split: 0.3.3 - stream-combiner: 0.0.4 - through: 2.3.8 - event-target-shim@5.0.1: {} eventemitter3@4.0.7: {} @@ -16660,14 +15923,6 @@ snapshots: fast-fifo@1.3.2: {} - fast-glob@3.3.1: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-glob@3.3.3: dependencies: '@nodelib/fs.stat': 2.0.5 @@ -16854,8 +16109,6 @@ snapshots: fresh@2.0.0: {} - from@0.1.7: {} - front-matter@4.0.2: dependencies: js-yaml: 3.14.1 @@ -17482,10 +16735,6 @@ snapshots: call-bound: 1.0.3 has-tostringtag: 1.0.2 - is-bun-module@1.2.1: - dependencies: - semver: 7.6.3 - is-callable@1.2.7: {} is-ci@3.0.1: @@ -17733,8 +16982,6 @@ snapshots: merge-stream: 2.0.0 supports-color: 8.1.1 - jose@5.9.6: {} - jose@6.0.8: {} joycon@3.1.1: {} @@ -17914,8 +17161,6 @@ snapshots: just-diff@6.0.2: {} - just-extend@6.2.0: {} - jwa@1.4.1: dependencies: buffer-equal-constant-time: 1.0.1 @@ -18164,8 +17409,6 @@ snapshots: lodash.flatten@4.4.0: {} - lodash.get@4.4.2: {} - lodash.groupby@4.6.0: {} lodash.includes@4.3.0: {} @@ -18295,8 +17538,6 @@ snapshots: map-obj@4.3.0: {} - map-stream@0.1.0: {} - markdown-it@14.1.0: dependencies: argparse: 2.0.1 @@ -18708,19 +17949,12 @@ snapshots: minipass: 3.3.6 yallist: 4.0.0 - minizlib@3.0.1: - dependencies: - minipass: 7.1.2 - rimraf: 5.0.10 - mkdirp@0.5.6: dependencies: minimist: 1.2.8 mkdirp@1.0.4: {} - mkdirp@3.0.1: {} - modify-values@1.0.1: {} monaco-editor@0.52.2: {} @@ -18819,17 +18053,6 @@ snapshots: nested-error-stacks@2.1.1: {} - next-auth@5.0.0-beta.25(next@15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): - dependencies: - '@auth/core': 0.37.2 - next: 15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - - next-router-mock@0.9.13(next@15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0))(react@19.0.0): - dependencies: - next: 15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) - react: 19.0.0 - next@15.2.3(@babel/core@7.26.10)(@playwright/test@1.47.2)(babel-plugin-macros@3.1.0)(react-dom@19.0.0(react@19.0.0))(react@19.0.0): dependencies: '@next/env': 15.2.3 @@ -18856,14 +18079,6 @@ snapshots: - '@babel/core' - babel-plugin-macros - nise@6.1.1: - dependencies: - '@sinonjs/commons': 3.0.1 - '@sinonjs/fake-timers': 13.0.5 - '@sinonjs/text-encoding': 0.7.3 - just-extend: 6.2.0 - path-to-regexp: 8.2.0 - node-cleanup@2.1.2: {} node-dir@0.1.17: @@ -19424,10 +18639,6 @@ snapshots: pathval@2.0.0: {} - pause-stream@0.0.11: - dependencies: - through: 2.3.8 - perf-cascade@3.0.3: dependencies: '@types/har-format': 1.2.16 @@ -19515,7 +18726,8 @@ snapshots: playwright-core@1.47.2: {} - playwright-core@1.48.2: {} + playwright-core@1.48.2: + optional: true playwright@1.47.2: dependencies: @@ -19528,6 +18740,7 @@ snapshots: playwright-core: 1.48.2 optionalDependencies: fsevents: 2.3.2 + optional: true pluralize@3.1.0: {} @@ -19582,17 +18795,10 @@ snapshots: postgres-range@1.1.4: {} - preact-render-to-string@5.2.3(preact@10.11.3): - dependencies: - preact: 10.11.3 - pretty-format: 3.8.0 - preact-render-to-string@6.5.11(preact@10.24.3): dependencies: preact: 10.24.3 - preact@10.11.3: {} - preact@10.24.3: {} prelude-ls@1.2.1: {} @@ -19615,8 +18821,6 @@ snapshots: ansi-styles: 5.2.0 react-is: 18.3.1 - pretty-format@3.8.0: {} - pretty-ms@9.1.0: dependencies: parse-ms: 4.0.0 @@ -19684,10 +18888,6 @@ snapshots: proxy-from-env@1.1.0: {} - ps-tree@1.2.0: - dependencies: - event-stream: 3.3.4 - psl@1.9.0: {} punycode.js@2.3.1: {} @@ -20151,10 +19351,6 @@ snapshots: dependencies: glob: 9.3.5 - rimraf@5.0.10: - dependencies: - glob: 10.4.5 - rimraf@6.0.1: dependencies: glob: 11.0.0 @@ -20439,15 +19635,6 @@ snapshots: dependencies: is-arrayish: 0.3.2 - sinon@19.0.2: - dependencies: - '@sinonjs/commons': 3.0.1 - '@sinonjs/fake-timers': 13.0.5 - '@sinonjs/samsam': 8.0.2 - diff: 7.0.0 - nise: 6.1.1 - supports-color: 7.2.0 - sirv@2.0.4: dependencies: '@polka/url': 1.0.0-next.28 @@ -20526,10 +19713,6 @@ snapshots: split2@4.2.0: {} - split@0.3.3: - dependencies: - through: 2.3.8 - split@1.0.1: dependencies: through: 2.3.8 @@ -20550,10 +19733,6 @@ snapshots: std-env@3.8.0: {} - stream-combiner@0.0.4: - dependencies: - duplexer: 0.1.2 - streamsearch@1.1.0: {} streamx@2.20.1: @@ -20804,21 +19983,8 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - tar@7.4.3: - dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.2 - minizlib: 3.0.1 - mkdirp: 3.0.1 - yallist: 5.0.0 - temp-dir@1.0.0: {} - terminate@2.8.0: - dependencies: - ps-tree: 1.2.0 - terser-webpack-plugin@5.3.11(esbuild@0.24.2)(webpack@5.98.0(esbuild@0.24.2)): dependencies: '@jridgewell/trace-mapping': 0.3.25 @@ -21046,10 +20212,6 @@ snapshots: dependencies: prelude-ls: 1.2.1 - type-detect@4.0.8: {} - - type-detect@4.1.0: {} - type-fest@0.18.1: {} type-fest@0.20.2: {} @@ -21643,8 +20805,6 @@ snapshots: yallist@4.0.0: {} - yallist@5.0.0: {} - yaml-diff-patch@2.0.0: dependencies: fast-json-patch: 3.1.1 @@ -21707,12 +20867,6 @@ snapshots: dependencies: zod: 3.23.8 - zod-validation-error@3.4.0(zod@3.24.2): - dependencies: - zod: 3.24.2 - zod@3.23.8: {} - zod@3.24.2: {} - zwitch@2.0.4: {} diff --git a/test/package.json b/test/package.json index eb4a49a9cc6..8b5fea581bd 100644 --- a/test/package.json +++ b/test/package.json @@ -4,7 +4,7 @@ "devDependencies": { "@mui/material": "6.4.7", "@toolpad/studio": "workspace:*", - "@toolpad/utils": "workspace:*", + "@toolpad/utils": "0.13.0", "@types/invariant": "2.2.37", "express": "4.21.2", "get-port": "7.1.0", From ef479e7f52cd8f0006d575f1c8ee0feca9a093b1 Mon Sep 17 00:00:00 2001 From: MUI bot <2109932+Janpot@users.noreply.github.com> Date: Thu, 20 Mar 2025 16:31:27 +0100 Subject: [PATCH 2/2] ci fixes --- package.json | 2 +- packages/toolpad-studio/tsconfig.json | 3 +-- tsconfig.json | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 35b11b11aa3..4c658f20a86 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "docs:build": "pnpm --filter docs build", "docs:build:api:core": "tsx --tsconfig ./scripts/tsconfig.json ./scripts/docs/buildCoreApiDocs/index.ts", "docs:build:api:studio": "tsx --tsconfig ./scripts/tsconfig.json ./scripts/docs/buildStudioApi.ts", - "docs:build:api": "concurrently \"pnpm docs:build:api:core\" \"pnpm docs:build:api:studio\"", + "docs:build:api": "concurrent \"pnpm docs:build:api:studio\"", "docs:typescript:formatted": "tsx ./docs/scripts/formattedTSDemos", "eslint": "eslint . --report-unused-disable-directives --ext .js,.ts,.tsx", "jsonlint": "node ./scripts/jsonlint.mjs", diff --git a/packages/toolpad-studio/tsconfig.json b/packages/toolpad-studio/tsconfig.json index 4f541a7dcb5..a083696d6d1 100644 --- a/packages/toolpad-studio/tsconfig.json +++ b/packages/toolpad-studio/tsconfig.json @@ -22,6 +22,5 @@ "incremental": true }, "include": ["src/**/*.ts", "src/**/*.tsx", "cli/**/*.ts", "cli/**/*.tsx", "typings/**/*.d.ts"], - "exclude": ["node_modules", "./scripts", "./public"], - "references": [{ "path": "../toolpad-utils" }, { "path": "../toolpad-studio-runtime" }] + "exclude": ["node_modules", "./scripts", "./public"] } diff --git a/tsconfig.json b/tsconfig.json index eacbd09f0e7..4d8e065a7c2 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,8 +18,6 @@ "types": ["node"], "baseUrl": "./", "paths": { - "@toolpad/core": ["./packages/toolpad-core/src"], - "@toolpad/core/*": ["./packages/toolpad-core/src/*"], "docs-toolpad/*": ["./docs/*"], "@mui-internal/api-docs-builder": ["./node_modules/@mui/monorepo/packages/api-docs-builder"], "@mui-internal/api-docs-builder/*": [