create-ej-app 26.0.0 → 28.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +125 -226
- package/cli.ts +79 -29
- package/package.json +2 -2
- package/template/api/.vscode/settings.json +1 -1
- package/template/api/{.github/copilot-instructions.md → AGENTS.md} +0 -2
- package/template/api/CLAUDE.md +1 -0
- package/template/api/package.json +10 -10
- package/template/nextjs/.dockerignore +1 -0
- package/template/nextjs/AGENTS.md +260 -0
- package/template/nextjs/CLAUDE.md +1 -0
- package/template/nextjs/README.md +109 -0
- package/template/nextjs/bunfig.toml +3 -0
- package/template/nextjs/eslint.config.mjs +6 -5
- package/template/nextjs/gitignore +6 -1
- package/template/nextjs/next.config.ts +8 -1
- package/template/nextjs/package.json +47 -39
- package/template/nextjs/public/logo-dark.svg +4 -0
- package/template/nextjs/public/logo.svg +4 -0
- package/template/nextjs/src/app/(admin)/actions/users.ts +183 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-header.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-search.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/_components/admin-sidebar.tsx +42 -0
- package/template/nextjs/src/app/(admin)/admin/layout.tsx +53 -0
- package/template/nextjs/src/app/(admin)/admin/page.tsx +11 -2
- package/template/nextjs/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/page.tsx +377 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/nextjs/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/nextjs/src/app/(app)/app/layout.tsx +44 -0
- package/template/nextjs/src/app/(app)/app/page.tsx +14 -2
- package/template/nextjs/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/nextjs/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/nextjs/src/app/(auth)/login/email-login-form.tsx +56 -29
- package/template/nextjs/src/app/(auth)/login/page.tsx +45 -12
- package/template/nextjs/src/app/(auth)/login/social-login-buttons.tsx +85 -17
- package/template/nextjs/src/app/(marketing)/layout.tsx +35 -4
- package/template/nextjs/src/app/(marketing)/legal.tsx +56 -0
- package/template/nextjs/src/app/(marketing)/page.tsx +20 -2
- package/template/nextjs/src/app/(marketing)/privacy/page.tsx +43 -2
- package/template/nextjs/src/app/(marketing)/terms/page.tsx +33 -2
- package/template/nextjs/src/app/error.tsx +37 -0
- package/template/nextjs/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/nextjs/src/app/fonts/index.ts +60 -0
- package/template/nextjs/src/app/global-error.tsx +38 -0
- package/template/nextjs/src/app/globals.css +29 -0
- package/template/nextjs/src/app/layout.tsx +45 -18
- package/template/nextjs/src/app/not-found.tsx +25 -3
- package/template/nextjs/src/app/robots.ts +29 -0
- package/template/nextjs/src/app/sitemap.ts +21 -0
- package/template/nextjs/src/components/confirm-dialog.tsx +79 -0
- package/template/nextjs/src/components/dashboard-header.tsx +70 -0
- package/template/nextjs/src/components/data-table/enum.ts +5 -0
- package/template/nextjs/src/components/data-table/features.ts +49 -0
- package/template/nextjs/src/components/data-table/index.tsx +961 -0
- package/template/nextjs/src/components/data-table/types.ts +76 -0
- package/template/nextjs/src/components/error.tsx +71 -0
- package/template/nextjs/src/components/loader.tsx +18 -2
- package/template/nextjs/src/components/logo.tsx +49 -0
- package/template/nextjs/src/components/navigation-search.tsx +140 -0
- package/template/nextjs/src/components/navigation-sidebar.tsx +241 -0
- package/template/nextjs/src/components/page-heading.tsx +38 -0
- package/template/nextjs/src/components/react-query-provider.tsx +41 -2
- package/template/nextjs/src/components/route-error.tsx +55 -0
- package/template/nextjs/src/components/screen-size.tsx +36 -0
- package/template/nextjs/src/components/site-footer.tsx +20 -4
- package/template/nextjs/src/components/theme-provider.tsx +48 -0
- package/template/nextjs/src/components/theme-toggle.tsx +58 -5
- package/template/nextjs/src/components/ui/alert-dialog.tsx +199 -0
- package/template/nextjs/src/components/ui/avatar.tsx +112 -0
- package/template/nextjs/src/components/ui/badge.tsx +54 -0
- package/template/nextjs/src/components/ui/button.tsx +3 -1
- package/template/nextjs/src/components/ui/card.tsx +4 -4
- package/template/nextjs/src/components/ui/collapsible.tsx +33 -0
- package/template/nextjs/src/components/ui/command.tsx +192 -0
- package/template/nextjs/src/components/ui/dialog.tsx +164 -0
- package/template/nextjs/src/components/ui/dropdown-menu.tsx +273 -0
- package/template/nextjs/src/components/ui/input-group.tsx +156 -0
- package/template/nextjs/src/components/ui/select.tsx +195 -0
- package/template/nextjs/src/components/ui/sheet.tsx +143 -0
- package/template/nextjs/src/components/ui/sidebar.tsx +702 -0
- package/template/nextjs/src/components/ui/skeleton.tsx +13 -0
- package/template/nextjs/src/components/ui/table.tsx +116 -0
- package/template/nextjs/src/components/ui/textarea.tsx +18 -0
- package/template/nextjs/src/components/ui/tooltip.tsx +57 -0
- package/template/nextjs/src/components/user-menu.tsx +206 -0
- package/template/nextjs/src/db/common.ts +10 -0
- package/template/nextjs/src/db/index.ts +16 -6
- package/template/nextjs/src/db/relations.ts +21 -0
- package/template/nextjs/src/db/schema.ts +85 -46
- package/template/nextjs/src/emails/components/email-layout.tsx +27 -11
- package/template/nextjs/src/emails/magic-link.tsx +15 -13
- package/template/nextjs/src/emails/test.tsx +11 -9
- package/template/nextjs/src/emails/welcome.tsx +12 -9
- package/template/nextjs/src/env.ts +1 -1
- package/template/nextjs/src/hooks/use-mobile.ts +14 -12
- package/template/nextjs/src/instrumentation.ts +20 -2
- package/template/nextjs/src/lib/actions.ts +55 -0
- package/template/nextjs/src/lib/admin-navigation.ts +43 -0
- package/template/nextjs/src/lib/auth-client.ts +20 -9
- package/template/nextjs/src/lib/auth-permissions.ts +61 -0
- package/template/nextjs/src/lib/auth.ts +33 -3
- package/template/nextjs/src/lib/callback-url.ts +28 -0
- package/template/nextjs/src/lib/constants.ts +21 -0
- package/template/nextjs/src/lib/delay.ts +10 -0
- package/template/nextjs/src/lib/email-service.ts +21 -0
- package/template/nextjs/src/lib/errors.ts +74 -0
- package/template/nextjs/src/lib/format-currency.ts +22 -24
- package/template/nextjs/src/lib/format-date.ts +69 -6
- package/template/nextjs/src/lib/nodemailer.ts +27 -3
- package/template/nextjs/src/lib/otel.ts +92 -2
- package/template/nextjs/src/lib/safe-action-client.ts +60 -9
- package/template/nextjs/src/lib/safe-action.ts +152 -16
- package/template/nextjs/src/lib/search.ts +16 -0
- package/template/nextjs/src/lib/seo.ts +7 -0
- package/template/nextjs/src/lib/siteConfig.ts +17 -1
- package/template/nextjs/src/lib/toast-message.ts +87 -1
- package/template/nextjs/src/lib/validations.ts +30 -0
- package/template/nextjs/src/proxy.ts +48 -0
- package/template/nextjs/src/scripts/reset-db.ts +22 -0
- package/template/nextjs/src/scripts/seed.ts +59 -8
- package/template/nextjs/src/scripts/test-mail.ts +28 -20
- package/template/nextjs/tests/admin/users.test.ts +64 -0
- package/template/nextjs/tests/db/fk-indexes.test.ts +62 -0
- package/template/nextjs/tests/db/seed.test.ts +49 -0
- package/template/nextjs/tests/helpers/action.ts +71 -0
- package/template/nextjs/tests/helpers/fixtures.ts +78 -0
- package/template/nextjs/tests/helpers/index.ts +2 -0
- package/template/nextjs/tests/lib/callback-url.test.ts +48 -0
- package/template/nextjs/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/nextjs/tests/lib/errors.test.ts +10 -0
- package/template/nextjs/tests/lib/search.test.ts +23 -0
- package/template/nextjs/tests/setup/pglite-db.ts +63 -0
- package/template/nextjs/tests/setup/preload.ts +95 -0
- package/template/nextjs/tsconfig.json +1 -0
- package/template/saas-starter/.dockerignore +1 -0
- package/template/saas-starter/.env.example +15 -15
- package/template/saas-starter/AGENTS.md +302 -0
- package/template/saas-starter/CLAUDE.md +1 -0
- package/template/saas-starter/README.md +124 -2
- package/template/saas-starter/bunfig.toml +3 -0
- package/template/saas-starter/eslint.config.mjs +6 -5
- package/template/saas-starter/gitignore +6 -1
- package/template/saas-starter/next.config.ts +19 -1
- package/template/saas-starter/package.json +50 -44
- package/template/saas-starter/public/logo-dark.svg +4 -0
- package/template/saas-starter/public/logo.svg +4 -0
- package/template/saas-starter/src/app/(admin)/actions/contact-leads.ts +51 -0
- package/template/saas-starter/src/app/(admin)/actions/feedbacks.ts +91 -0
- package/template/saas-starter/src/app/(admin)/actions/users.ts +145 -59
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-header.tsx +4 -1
- package/template/saas-starter/src/app/(admin)/admin/_components/admin-sidebar.tsx +12 -10
- package/template/saas-starter/src/app/(admin)/admin/contact-leads/page.tsx +123 -0
- package/template/saas-starter/src/app/(admin)/admin/feedbacks/page.tsx +161 -0
- package/template/saas-starter/src/app/(admin)/admin/layout.tsx +24 -28
- package/template/saas-starter/src/app/(admin)/admin/page.tsx +59 -2
- package/template/saas-starter/src/app/(admin)/admin/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/[userId]/page.tsx +43 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/new/page.tsx +16 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/page.tsx +372 -10
- package/template/saas-starter/src/app/(admin)/admin/settings/users/user-form.tsx +138 -0
- package/template/saas-starter/src/app/(admin)/admin/settings/users/validation.ts +23 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/[ticketId]/page.tsx +17 -0
- package/template/saas-starter/src/app/(admin)/admin/support-tickets/page.tsx +5 -0
- package/template/saas-starter/src/app/(app)/actions/billing.ts +171 -107
- package/template/saas-starter/src/app/(app)/actions/feedbacks.ts +28 -0
- package/template/saas-starter/src/app/(app)/actions/feedbacks.validation.ts +12 -0
- package/template/saas-starter/src/app/(app)/actions/members.ts +149 -123
- package/template/saas-starter/src/app/(app)/actions/organizations.ts +230 -0
- package/template/saas-starter/src/app/(app)/actions/organizations.validation.ts +17 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/dashboard/page.tsx +6 -9
- package/template/saas-starter/src/app/(app)/app/[orgId]/layout.tsx +6 -9
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/billing-email-form.tsx +45 -53
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/danger-zone.tsx +40 -47
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/general-settings-form.tsx +80 -105
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/_components/plans-section.tsx +72 -82
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/billing/page.tsx +51 -52
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/layout.tsx +24 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/invite-member-dialog.tsx +15 -16
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/_components/members-tabs.tsx +77 -44
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/members/page.tsx +11 -18
- package/template/saas-starter/src/app/(app)/app/[orgId]/settings/page.tsx +14 -28
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/[ticketId]/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/[orgId]/support-tickets/page.tsx +25 -0
- package/template/saas-starter/src/app/(app)/app/_components/app-header.tsx +15 -13
- package/template/saas-starter/src/app/(app)/app/_components/app-search.tsx +15 -6
- package/template/saas-starter/src/app/(app)/app/_components/app-sidebar.tsx +6 -3
- package/template/saas-starter/src/app/(app)/app/_components/feedback-dialog.tsx +108 -0
- package/template/saas-starter/src/app/(app)/app/_components/organization-context.tsx +5 -9
- package/template/saas-starter/src/app/(app)/app/_components/organization-switcher.tsx +68 -8
- package/template/saas-starter/src/app/(app)/app/layout.tsx +44 -32
- package/template/saas-starter/src/app/(app)/app/onboarding/_components/create-organization-form.tsx +75 -87
- package/template/saas-starter/src/app/(app)/app/onboarding/page.tsx +10 -12
- package/template/saas-starter/src/app/(app)/app/page.tsx +16 -21
- package/template/saas-starter/src/app/(auth)/(account)/layout.tsx +64 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/page.tsx +16 -0
- package/template/saas-starter/src/app/(auth)/(account)/profile/profile-form.tsx +247 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/[ticketId]/page.tsx +19 -0
- package/template/saas-starter/src/app/(auth)/(account)/support/page.tsx +9 -0
- package/template/saas-starter/src/app/(auth)/login/email-login-form.tsx +49 -27
- package/template/saas-starter/src/app/(auth)/login/page.tsx +39 -21
- package/template/saas-starter/src/app/(auth)/login/social-login-buttons.tsx +80 -18
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/accept-invitation-form.tsx +16 -10
- package/template/saas-starter/src/app/(invitation)/accept-invitation/[id]/page.tsx +28 -42
- package/template/saas-starter/src/app/(invitation)/actions/invitations.ts +32 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/actions.ts +23 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/contact-form.tsx +118 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/page.tsx +20 -0
- package/template/saas-starter/src/app/(marketing)/contact-us/validation.ts +22 -0
- package/template/saas-starter/src/app/(marketing)/layout.tsx +38 -4
- package/template/saas-starter/src/app/(marketing)/legal.tsx +66 -0
- package/template/saas-starter/src/app/(marketing)/page.tsx +23 -2
- package/template/saas-starter/src/app/(marketing)/privacy/page.tsx +62 -2
- package/template/saas-starter/src/app/(marketing)/terms/page.tsx +40 -2
- package/template/saas-starter/src/app/actions/support-tickets.ts +510 -0
- package/template/saas-starter/src/app/actions/support-tickets.validation.ts +112 -0
- package/template/saas-starter/src/app/actions/uploads.ts +36 -0
- package/template/saas-starter/src/app/actions/uploads.validation.ts +24 -0
- package/template/saas-starter/src/app/error.tsx +37 -0
- package/template/saas-starter/src/app/fonts/geist-mono/GeistMono-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/geist-sans/Geist-Variable.woff2 +0 -0
- package/template/saas-starter/src/app/fonts/index.ts +60 -0
- package/template/saas-starter/src/app/global-error.tsx +38 -0
- package/template/saas-starter/src/app/globals.css +29 -0
- package/template/saas-starter/src/app/layout.tsx +43 -19
- package/template/saas-starter/src/app/not-found.tsx +25 -3
- package/template/saas-starter/src/app/robots.ts +14 -10
- package/template/saas-starter/src/app/sitemap.ts +2 -1
- package/template/saas-starter/src/components/confirm-dialog.tsx +79 -0
- package/template/saas-starter/src/components/dashboard-header.tsx +7 -1
- package/template/saas-starter/src/components/data-table/features.ts +49 -0
- package/template/saas-starter/src/components/data-table/index.tsx +65 -64
- package/template/saas-starter/src/components/data-table/types.ts +5 -4
- package/template/saas-starter/src/components/error.tsx +45 -0
- package/template/saas-starter/src/components/file-upload.tsx +65 -26
- package/template/saas-starter/src/components/loader.tsx +18 -2
- package/template/saas-starter/src/components/logo.tsx +49 -0
- package/template/saas-starter/src/components/navigation-search.tsx +2 -2
- package/template/saas-starter/src/components/page-heading.tsx +6 -0
- package/template/saas-starter/src/components/react-query-provider.tsx +41 -2
- package/template/saas-starter/src/components/route-error.tsx +55 -0
- package/template/saas-starter/src/components/screen-size.tsx +36 -0
- package/template/saas-starter/src/components/site-footer.tsx +20 -4
- package/template/saas-starter/src/components/support/create-support-ticket-dialog.tsx +122 -0
- package/template/saas-starter/src/components/support/support-ticket-conversation.tsx +236 -0
- package/template/saas-starter/src/components/support/support-ticket-shared.tsx +58 -0
- package/template/saas-starter/src/components/support/support-tickets-table.tsx +281 -0
- package/template/saas-starter/src/components/theme-provider.tsx +48 -0
- package/template/saas-starter/src/components/theme-toggle.tsx +58 -5
- package/template/saas-starter/src/components/ui/alert-dialog.tsx +199 -0
- package/template/saas-starter/src/components/ui/badge.tsx +6 -1
- package/template/saas-starter/src/components/ui/button.tsx +2 -0
- package/template/saas-starter/src/components/ui/command.tsx +1 -1
- package/template/saas-starter/src/components/user-menu.tsx +147 -24
- package/template/saas-starter/src/db/common.ts +33 -1
- package/template/saas-starter/src/db/enums.ts +24 -0
- package/template/saas-starter/src/db/index.ts +14 -5
- package/template/saas-starter/src/db/relations.ts +40 -3
- package/template/saas-starter/src/db/schema.ts +186 -35
- package/template/saas-starter/src/emails/components/email-layout.tsx +27 -11
- package/template/saas-starter/src/emails/invitation.tsx +34 -21
- package/template/saas-starter/src/emails/magic-link.tsx +15 -13
- package/template/saas-starter/src/emails/test.tsx +11 -9
- package/template/saas-starter/src/emails/welcome.tsx +12 -9
- package/template/saas-starter/src/env.ts +1 -1
- package/template/saas-starter/src/hooks/use-mobile.ts +14 -12
- package/template/saas-starter/src/instrumentation.ts +20 -2
- package/template/saas-starter/src/lib/actions.ts +34 -0
- package/template/saas-starter/src/lib/admin-navigation.ts +33 -1
- package/template/saas-starter/src/lib/app-navigation.ts +45 -41
- package/template/saas-starter/src/lib/auth-client.ts +8 -3
- package/template/saas-starter/src/lib/auth-permissions.ts +50 -11
- package/template/saas-starter/src/lib/auth.ts +122 -21
- package/template/saas-starter/src/lib/billing.ts +59 -24
- package/template/saas-starter/src/lib/callback-url.ts +28 -0
- package/template/saas-starter/src/lib/constants.ts +78 -9
- package/template/saas-starter/src/lib/delay.ts +10 -0
- package/template/saas-starter/src/lib/email-service.ts +30 -1
- package/template/saas-starter/src/lib/errors.ts +74 -0
- package/template/saas-starter/src/lib/format-currency.ts +22 -24
- package/template/saas-starter/src/lib/format-date.ts +69 -6
- package/template/saas-starter/src/lib/image-url.ts +54 -0
- package/template/saas-starter/src/lib/nodemailer.ts +27 -3
- package/template/saas-starter/src/lib/organization-access.ts +56 -76
- package/template/saas-starter/src/lib/otel.ts +92 -2
- package/template/saas-starter/src/lib/plan-limits.ts +37 -0
- package/template/saas-starter/src/lib/plans.ts +40 -13
- package/template/saas-starter/src/lib/rbac.ts +44 -0
- package/template/saas-starter/src/lib/safe-action-client.ts +60 -9
- package/template/saas-starter/src/lib/safe-action.ts +137 -17
- package/template/saas-starter/src/lib/search.ts +16 -0
- package/template/saas-starter/src/lib/seo.ts +7 -0
- package/template/saas-starter/src/lib/siteConfig.ts +17 -1
- package/template/saas-starter/src/lib/storage.ts +176 -46
- package/template/saas-starter/src/lib/stripe.ts +4 -1
- package/template/saas-starter/src/lib/toast-message.ts +87 -1
- package/template/saas-starter/src/lib/validations.ts +85 -2
- package/template/saas-starter/src/proxy.ts +48 -0
- package/template/saas-starter/src/scripts/cleanup-temp-uploads.ts +24 -0
- package/template/saas-starter/src/scripts/seed.ts +82 -8
- package/template/saas-starter/src/scripts/setup-db.ts +72 -0
- package/template/saas-starter/src/scripts/setup-stripe-plans.ts +13 -7
- package/template/saas-starter/src/scripts/test-mail.ts +28 -20
- package/template/saas-starter/tests/admin/users.test.ts +64 -0
- package/template/saas-starter/tests/db/fk-indexes.test.ts +62 -0
- package/template/saas-starter/tests/db/seed.test.ts +57 -0
- package/template/saas-starter/tests/helpers/action.ts +71 -0
- package/template/saas-starter/tests/helpers/fixtures.ts +151 -0
- package/template/saas-starter/tests/helpers/index.ts +2 -0
- package/template/saas-starter/tests/lib/callback-url.test.ts +48 -0
- package/template/saas-starter/tests/lib/client-server-boundary.test.ts +57 -0
- package/template/saas-starter/tests/lib/errors.test.ts +10 -0
- package/template/saas-starter/tests/lib/image-url.test.ts +31 -0
- package/template/saas-starter/tests/lib/search.test.ts +23 -0
- package/template/saas-starter/tests/marketing/contact-and-feedback.test.ts +86 -0
- package/template/saas-starter/tests/organizations/organizations.test.ts +175 -0
- package/template/saas-starter/tests/setup/pglite-db.ts +67 -0
- package/template/saas-starter/tests/setup/preload.ts +108 -0
- package/template/saas-starter/tests/support/support-tickets.test.ts +145 -0
- package/template/saas-starter/tsconfig.json +1 -0
- package/cli.js +0 -147
- package/template/nextjs/src/app/(admin)/layout.tsx +0 -20
- package/template/nextjs/src/app/(app)/layout.tsx +0 -20
- package/template/nextjs/terraform/artifact-registry.tf +0 -5
- package/template/nextjs/terraform/backend-service.tf +0 -13
- package/template/nextjs/terraform/cleanup.sh +0 -9
- package/template/nextjs/terraform/cloudbuild.tf +0 -70
- package/template/nextjs/terraform/cloudrun.tf +0 -68
- package/template/nextjs/terraform/database.tf +0 -60
- package/template/nextjs/terraform/forwarding-rule.tf +0 -43
- package/template/nextjs/terraform/gitignore +0 -4
- package/template/nextjs/terraform/ip-address.tf +0 -11
- package/template/nextjs/terraform/network-endpoint-group.tf +0 -11
- package/template/nextjs/terraform/network.tf +0 -21
- package/template/nextjs/terraform/pre.sh +0 -3
- package/template/nextjs/terraform/providers.tf +0 -18
- package/template/nextjs/terraform/secret.txt +0 -11
- package/template/nextjs/terraform/setup.sh +0 -25
- package/template/nextjs/terraform/ssl-certs.tf +0 -32
- package/template/nextjs/terraform/target-https-proxy copy.tf +0 -14
- package/template/nextjs/terraform/target-https-proxy.tf +0 -14
- package/template/nextjs/terraform/terraform.tfvars +0 -4
- package/template/nextjs/terraform/url-map.tf +0 -23
- package/template/nextjs/terraform/variables.tf +0 -21
- package/template/saas-starter/next-env.d.ts +0 -6
- package/template/saas-starter/src/actions/uploads.ts +0 -28
- package/template/saas-starter/src/actions/uploads.validation.ts +0 -14
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/create-user-dialog.tsx +0 -183
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/edit-name-dialog.tsx +0 -129
- package/template/saas-starter/src/app/(admin)/admin/settings/users/_components/users-table.tsx +0 -415
- package/template/saas-starter/src/app/(app)/actions/organization.ts +0 -92
- package/template/saas-starter/src/app/proxy.ts +0 -28
- package/template/saas-starter/src/components/impersonation-banner.tsx +0 -44
- package/template/saas-starter/src/lib/organization-plan-limits.ts +0 -38
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { notFound } from 'next/navigation'
|
|
2
|
+
import { getOrganizationCached } from '~/app/(app)/actions/organizations'
|
|
3
|
+
import { isManagerRole } from '~/lib/rbac'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Settings are for owners and admins. Every action behind these pages checks
|
|
7
|
+
* the role again; this guard only keeps members off pages they cannot use.
|
|
8
|
+
*/
|
|
9
|
+
export default async function SettingsLayout({
|
|
10
|
+
children,
|
|
11
|
+
params
|
|
12
|
+
}: {
|
|
13
|
+
children: React.ReactNode
|
|
14
|
+
params: Promise<{ orgId: string }>
|
|
15
|
+
}) {
|
|
16
|
+
const { orgId } = await params
|
|
17
|
+
const org = (await getOrganizationCached(orgId))?.data
|
|
18
|
+
|
|
19
|
+
if (!org || !isManagerRole(org.role)) {
|
|
20
|
+
return notFound()
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return <>{children}</>
|
|
24
|
+
}
|
|
@@ -30,13 +30,14 @@ import {
|
|
|
30
30
|
SelectValue
|
|
31
31
|
} from '~/components/ui/select'
|
|
32
32
|
import { organization } from '~/lib/auth-client'
|
|
33
|
-
import {
|
|
33
|
+
import { ASSIGNABLE_ROLES, ROLE_DESCRIPTIONS } from '~/lib/rbac'
|
|
34
|
+
import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
|
|
34
35
|
import { emailValidation } from '~/lib/validations'
|
|
35
36
|
import { useOrganizationContext } from '../../../../_components/organization-context'
|
|
36
37
|
|
|
37
38
|
const schema = z.object({
|
|
38
39
|
email: emailValidation,
|
|
39
|
-
role: z.enum(
|
|
40
|
+
role: z.enum(ASSIGNABLE_ROLES)
|
|
40
41
|
})
|
|
41
42
|
|
|
42
43
|
type Form = z.infer<typeof schema>
|
|
@@ -62,6 +63,7 @@ export function InviteMemberDialog({
|
|
|
62
63
|
defaultValues: { email: '', role: 'member' }
|
|
63
64
|
})
|
|
64
65
|
|
|
66
|
+
// Seed once per open; see "Client data and forms" in AGENTS.md.
|
|
65
67
|
useEffect(() => {
|
|
66
68
|
if (open) reset({ email: '', role: 'member' })
|
|
67
69
|
}, [open, reset])
|
|
@@ -74,7 +76,7 @@ export function InviteMemberDialog({
|
|
|
74
76
|
})
|
|
75
77
|
|
|
76
78
|
if (error) {
|
|
77
|
-
|
|
79
|
+
toastActionError(error, 'The invitation was not sent. Try again.')
|
|
78
80
|
return
|
|
79
81
|
}
|
|
80
82
|
|
|
@@ -88,10 +90,10 @@ export function InviteMemberDialog({
|
|
|
88
90
|
<DialogTrigger asChild>{children}</DialogTrigger>
|
|
89
91
|
<DialogContent className="sm:max-w-md">
|
|
90
92
|
<DialogHeader>
|
|
91
|
-
<DialogTitle>Invite
|
|
93
|
+
<DialogTitle>Invite Member</DialogTitle>
|
|
92
94
|
<DialogDescription>
|
|
93
|
-
|
|
94
|
-
|
|
95
|
+
We email an invitation link. They accept it by signing in with this
|
|
96
|
+
address.
|
|
95
97
|
</DialogDescription>
|
|
96
98
|
</DialogHeader>
|
|
97
99
|
|
|
@@ -129,12 +131,11 @@ export function InviteMemberDialog({
|
|
|
129
131
|
<SelectValue placeholder="Select role" />
|
|
130
132
|
</SelectTrigger>
|
|
131
133
|
<SelectContent>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
</SelectItem>
|
|
134
|
+
{ASSIGNABLE_ROLES.map(role => (
|
|
135
|
+
<SelectItem key={role} value={role}>
|
|
136
|
+
{ROLE_DESCRIPTIONS[role]}
|
|
137
|
+
</SelectItem>
|
|
138
|
+
))}
|
|
138
139
|
</SelectContent>
|
|
139
140
|
</Select>
|
|
140
141
|
)}
|
|
@@ -156,10 +157,8 @@ export function InviteMemberDialog({
|
|
|
156
157
|
Cancel
|
|
157
158
|
</Button>
|
|
158
159
|
<Button type="submit" disabled={isSubmitting}>
|
|
159
|
-
{isSubmitting &&
|
|
160
|
-
|
|
161
|
-
)}
|
|
162
|
-
Send invitation
|
|
160
|
+
{isSubmitting && <Loader2 className="animate-spin" />}
|
|
161
|
+
Send Invitation
|
|
163
162
|
</Button>
|
|
164
163
|
</DialogFooter>
|
|
165
164
|
</form>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useQueryClient } from '@tanstack/react-query'
|
|
4
|
-
import { ColumnDef } from '@tanstack/react-table'
|
|
5
4
|
import {
|
|
6
5
|
MoreHorizontal,
|
|
7
6
|
RefreshCcw,
|
|
@@ -17,7 +16,8 @@ import {
|
|
|
17
16
|
useQueryStates
|
|
18
17
|
} from 'nuqs'
|
|
19
18
|
import { useState } from 'react'
|
|
20
|
-
import {
|
|
19
|
+
import { ConfirmDialog } from '~/components/confirm-dialog'
|
|
20
|
+
import { DataTable, type DataTableColumnDef } from '~/components/data-table'
|
|
21
21
|
import { SortOrderEnum } from '~/components/data-table/enum'
|
|
22
22
|
import { DataTableFilter } from '~/components/data-table/types'
|
|
23
23
|
import { Avatar, AvatarFallback, AvatarImage } from '~/components/ui/avatar'
|
|
@@ -38,8 +38,9 @@ import {
|
|
|
38
38
|
} from '~/components/ui/tooltip'
|
|
39
39
|
import { organization } from '~/lib/auth-client'
|
|
40
40
|
import { formatDate } from '~/lib/format-date'
|
|
41
|
+
import { roleArticle, type AssignableRole } from '~/lib/rbac'
|
|
41
42
|
import { useSafeActionQuery } from '~/lib/safe-action-client'
|
|
42
|
-
import {
|
|
43
|
+
import { toastActionError, toastSuccessMessage } from '~/lib/toast-message'
|
|
43
44
|
import { getInvitations, getMembers } from '../../../../../actions/members'
|
|
44
45
|
import { useOrganizationContext } from '../../../../_components/organization-context'
|
|
45
46
|
import { InviteMemberDialog } from './invite-member-dialog'
|
|
@@ -67,6 +68,10 @@ type Invitation = {
|
|
|
67
68
|
const MEMBERS_QUERY_KEY = 'org-members'
|
|
68
69
|
const INVITATIONS_QUERY_KEY = 'org-invitations'
|
|
69
70
|
|
|
71
|
+
type PendingConfirm =
|
|
72
|
+
| { kind: 'remove'; member: Member }
|
|
73
|
+
| { kind: 'cancel'; invitation: Invitation }
|
|
74
|
+
|
|
70
75
|
function roleVariant(role: string): 'default' | 'secondary' | 'outline' {
|
|
71
76
|
if (role === 'owner') return 'default'
|
|
72
77
|
if (role === 'admin') return 'secondary'
|
|
@@ -75,10 +80,11 @@ function roleVariant(role: string): 'default' | 'secondary' | 'outline' {
|
|
|
75
80
|
|
|
76
81
|
function statusVariant(
|
|
77
82
|
status: string
|
|
78
|
-
): '
|
|
79
|
-
if (status === 'accepted') return '
|
|
80
|
-
if (status === 'pending') return '
|
|
81
|
-
return 'destructive'
|
|
83
|
+
): 'success' | 'info' | 'secondary' | 'destructive' {
|
|
84
|
+
if (status === 'accepted') return 'success'
|
|
85
|
+
if (status === 'pending') return 'info'
|
|
86
|
+
if (status === 'rejected') return 'destructive'
|
|
87
|
+
return 'secondary'
|
|
82
88
|
}
|
|
83
89
|
|
|
84
90
|
function getInitials(name: string, email: string) {
|
|
@@ -129,6 +135,9 @@ export function MembersTabs({
|
|
|
129
135
|
const router = useRouter()
|
|
130
136
|
const queryClient = useQueryClient()
|
|
131
137
|
const [busyId, setBusyId] = useState<string | null>(null)
|
|
138
|
+
const [pendingConfirm, setPendingConfirm] = useState<PendingConfirm | null>(
|
|
139
|
+
null
|
|
140
|
+
)
|
|
132
141
|
|
|
133
142
|
const atMemberLimit = memberCount >= maxMembers
|
|
134
143
|
|
|
@@ -154,14 +163,9 @@ export function MembersTabs({
|
|
|
154
163
|
page: memberQuery.mPage,
|
|
155
164
|
limit: memberQuery.mLimit,
|
|
156
165
|
sortBy: (memberQuery.mSortBy ?? undefined) as
|
|
157
|
-
| '
|
|
158
|
-
| 'email'
|
|
159
|
-
| 'role'
|
|
160
|
-
| 'createdAt'
|
|
161
|
-
| undefined,
|
|
166
|
+
'name' | 'email' | 'role' | 'createdAt' | undefined,
|
|
162
167
|
sortOrder: (memberQuery.mSortOrder ?? undefined) as
|
|
163
|
-
|
|
|
164
|
-
| undefined,
|
|
168
|
+
SortOrderEnum | undefined,
|
|
165
169
|
search: memberQuery.mSearch ?? undefined,
|
|
166
170
|
filters: memberQuery.mFilters ?? undefined
|
|
167
171
|
}
|
|
@@ -172,7 +176,7 @@ export function MembersTabs({
|
|
|
172
176
|
memberParams
|
|
173
177
|
)
|
|
174
178
|
|
|
175
|
-
async function changeRole(member: Member, role:
|
|
179
|
+
async function changeRole(member: Member, role: AssignableRole) {
|
|
176
180
|
setBusyId(member.id)
|
|
177
181
|
const { error } = await organization.updateMemberRole({
|
|
178
182
|
organizationId: orgId,
|
|
@@ -181,15 +185,14 @@ export function MembersTabs({
|
|
|
181
185
|
})
|
|
182
186
|
setBusyId(null)
|
|
183
187
|
if (error) {
|
|
184
|
-
|
|
188
|
+
toastActionError(error, 'The role was not changed. Try again.')
|
|
185
189
|
return
|
|
186
190
|
}
|
|
187
|
-
toastSuccessMessage(`${member.name} is now ${role}`)
|
|
191
|
+
toastSuccessMessage(`${member.name} is now ${roleArticle(role)}`)
|
|
188
192
|
invalidateMembers()
|
|
189
193
|
}
|
|
190
194
|
|
|
191
195
|
async function removeMember(member: Member) {
|
|
192
|
-
if (!confirm(`Remove ${member.name} from this organization?`)) return
|
|
193
196
|
setBusyId(member.id)
|
|
194
197
|
const { error } = await organization.removeMember({
|
|
195
198
|
organizationId: orgId,
|
|
@@ -197,16 +200,17 @@ export function MembersTabs({
|
|
|
197
200
|
})
|
|
198
201
|
setBusyId(null)
|
|
199
202
|
if (error) {
|
|
200
|
-
|
|
203
|
+
toastActionError(error, 'The member was not removed. Try again.')
|
|
201
204
|
return
|
|
202
205
|
}
|
|
206
|
+
setPendingConfirm(null)
|
|
203
207
|
toastSuccessMessage(`${member.name} removed`)
|
|
204
208
|
invalidateMembers()
|
|
205
209
|
// Refresh the server component so the member-limit gate reflects the freed slot.
|
|
206
210
|
router.refresh()
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
const memberColumns:
|
|
213
|
+
const memberColumns: DataTableColumnDef<Member>[] = [
|
|
210
214
|
{
|
|
211
215
|
accessorKey: 'name',
|
|
212
216
|
header: 'Member',
|
|
@@ -285,18 +289,18 @@ export function MembersTabs({
|
|
|
285
289
|
<DropdownMenuContent align="end">
|
|
286
290
|
{member.role !== 'admin' ? (
|
|
287
291
|
<DropdownMenuItem onClick={() => changeRole(member, 'admin')}>
|
|
288
|
-
Make
|
|
292
|
+
Make Admin
|
|
289
293
|
</DropdownMenuItem>
|
|
290
294
|
) : null}
|
|
291
295
|
{member.role !== 'member' ? (
|
|
292
296
|
<DropdownMenuItem onClick={() => changeRole(member, 'member')}>
|
|
293
|
-
Make
|
|
297
|
+
Make Member
|
|
294
298
|
</DropdownMenuItem>
|
|
295
299
|
) : null}
|
|
296
300
|
<DropdownMenuSeparator />
|
|
297
301
|
<DropdownMenuItem
|
|
298
302
|
variant="destructive"
|
|
299
|
-
onClick={() =>
|
|
303
|
+
onClick={() => setPendingConfirm({ kind: 'remove', member })}
|
|
300
304
|
>
|
|
301
305
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
302
306
|
Remove
|
|
@@ -325,14 +329,9 @@ export function MembersTabs({
|
|
|
325
329
|
page: inviteQuery.iPage,
|
|
326
330
|
limit: inviteQuery.iLimit,
|
|
327
331
|
sortBy: (inviteQuery.iSortBy ?? undefined) as
|
|
328
|
-
| '
|
|
329
|
-
| 'status'
|
|
330
|
-
| 'createdAt'
|
|
331
|
-
| 'expiresAt'
|
|
332
|
-
| undefined,
|
|
332
|
+
'email' | 'status' | 'createdAt' | 'expiresAt' | undefined,
|
|
333
333
|
sortOrder: (inviteQuery.iSortOrder ?? undefined) as
|
|
334
|
-
|
|
|
335
|
-
| undefined,
|
|
334
|
+
SortOrderEnum | undefined,
|
|
336
335
|
search: inviteQuery.iSearch ?? undefined,
|
|
337
336
|
filters: inviteQuery.iFilters ?? undefined
|
|
338
337
|
}
|
|
@@ -353,7 +352,7 @@ export function MembersTabs({
|
|
|
353
352
|
})
|
|
354
353
|
setBusyId(null)
|
|
355
354
|
if (error) {
|
|
356
|
-
|
|
355
|
+
toastActionError(error, 'The invitation was not resent. Try again.')
|
|
357
356
|
return
|
|
358
357
|
}
|
|
359
358
|
toastSuccessMessage(`Invitation resent to ${invitation.email}`)
|
|
@@ -361,21 +360,21 @@ export function MembersTabs({
|
|
|
361
360
|
}
|
|
362
361
|
|
|
363
362
|
async function cancelInvitation(invitation: Invitation) {
|
|
364
|
-
if (!confirm(`Cancel the invitation for ${invitation.email}?`)) return
|
|
365
363
|
setBusyId(invitation.id)
|
|
366
364
|
const { error } = await organization.cancelInvitation({
|
|
367
365
|
invitationId: invitation.id
|
|
368
366
|
})
|
|
369
367
|
setBusyId(null)
|
|
370
368
|
if (error) {
|
|
371
|
-
|
|
369
|
+
toastActionError(error, 'The invitation was not canceled. Try again.')
|
|
372
370
|
return
|
|
373
371
|
}
|
|
374
|
-
|
|
372
|
+
setPendingConfirm(null)
|
|
373
|
+
toastSuccessMessage('Invitation canceled')
|
|
375
374
|
invalidateInvitations()
|
|
376
375
|
}
|
|
377
376
|
|
|
378
|
-
const invitationColumns:
|
|
377
|
+
const invitationColumns: DataTableColumnDef<Invitation>[] = [
|
|
379
378
|
{
|
|
380
379
|
accessorKey: 'email',
|
|
381
380
|
header: 'Email',
|
|
@@ -452,7 +451,9 @@ export function MembersTabs({
|
|
|
452
451
|
<DropdownMenuSeparator />
|
|
453
452
|
<DropdownMenuItem
|
|
454
453
|
variant="destructive"
|
|
455
|
-
onClick={() =>
|
|
454
|
+
onClick={() =>
|
|
455
|
+
setPendingConfirm({ kind: 'cancel', invitation })
|
|
456
|
+
}
|
|
456
457
|
>
|
|
457
458
|
<XCircle className="mr-2 h-4 w-4" />
|
|
458
459
|
Cancel
|
|
@@ -477,21 +478,21 @@ export function MembersTabs({
|
|
|
477
478
|
<TooltipTrigger asChild>
|
|
478
479
|
<span tabIndex={0}>
|
|
479
480
|
<Button size="sm" disabled>
|
|
480
|
-
<UserPlus
|
|
481
|
-
Invite
|
|
481
|
+
<UserPlus />
|
|
482
|
+
Invite Member
|
|
482
483
|
</Button>
|
|
483
484
|
</span>
|
|
484
485
|
</TooltipTrigger>
|
|
485
486
|
<TooltipContent>
|
|
486
|
-
|
|
487
|
-
plan to invite more
|
|
487
|
+
Your plan allows {maxMembers} members and you have {memberCount}.
|
|
488
|
+
Upgrade your plan to invite more.
|
|
488
489
|
</TooltipContent>
|
|
489
490
|
</Tooltip>
|
|
490
491
|
) : (
|
|
491
492
|
<InviteMemberDialog onInvited={invalidateInvitations}>
|
|
492
493
|
<Button size="sm">
|
|
493
|
-
<UserPlus
|
|
494
|
-
Invite
|
|
494
|
+
<UserPlus />
|
|
495
|
+
Invite Member
|
|
495
496
|
</Button>
|
|
496
497
|
</InviteMemberDialog>
|
|
497
498
|
)}
|
|
@@ -505,7 +506,7 @@ export function MembersTabs({
|
|
|
505
506
|
totalCount={memberData?.[1]}
|
|
506
507
|
enableSearch
|
|
507
508
|
initialSearch={memberParams.search}
|
|
508
|
-
searchPlaceholder="Search members
|
|
509
|
+
searchPlaceholder="Search members…"
|
|
509
510
|
manualPagination
|
|
510
511
|
manualSorting
|
|
511
512
|
pageIndex={memberParams.page - 1}
|
|
@@ -540,7 +541,7 @@ export function MembersTabs({
|
|
|
540
541
|
totalCount={inviteData?.[1]}
|
|
541
542
|
enableSearch
|
|
542
543
|
initialSearch={inviteParams.search}
|
|
543
|
-
searchPlaceholder="Search invitations
|
|
544
|
+
searchPlaceholder="Search invitations…"
|
|
544
545
|
manualPagination
|
|
545
546
|
manualSorting
|
|
546
547
|
pageIndex={inviteParams.page - 1}
|
|
@@ -566,6 +567,38 @@ export function MembersTabs({
|
|
|
566
567
|
}
|
|
567
568
|
/>
|
|
568
569
|
</TabsContent>
|
|
570
|
+
|
|
571
|
+
<ConfirmDialog
|
|
572
|
+
open={pendingConfirm !== null}
|
|
573
|
+
onOpenChange={open => {
|
|
574
|
+
if (!open) setPendingConfirm(null)
|
|
575
|
+
}}
|
|
576
|
+
title={
|
|
577
|
+
pendingConfirm?.kind === 'remove'
|
|
578
|
+
? 'Remove Member'
|
|
579
|
+
: 'Cancel Invitation'
|
|
580
|
+
}
|
|
581
|
+
description={
|
|
582
|
+
pendingConfirm?.kind === 'remove'
|
|
583
|
+
? `${pendingConfirm.member.name} loses access to this organization.`
|
|
584
|
+
: pendingConfirm?.kind === 'cancel'
|
|
585
|
+
? `The invitation link sent to ${pendingConfirm.invitation.email} stops working.`
|
|
586
|
+
: ''
|
|
587
|
+
}
|
|
588
|
+
confirmLabel={
|
|
589
|
+
pendingConfirm?.kind === 'remove'
|
|
590
|
+
? 'Remove Member'
|
|
591
|
+
: 'Cancel Invitation'
|
|
592
|
+
}
|
|
593
|
+
pending={busyId !== null}
|
|
594
|
+
onConfirm={() => {
|
|
595
|
+
if (pendingConfirm?.kind === 'remove') {
|
|
596
|
+
void removeMember(pendingConfirm.member)
|
|
597
|
+
} else if (pendingConfirm?.kind === 'cancel') {
|
|
598
|
+
void cancelInvitation(pendingConfirm.invitation)
|
|
599
|
+
}
|
|
600
|
+
}}
|
|
601
|
+
/>
|
|
569
602
|
</Tabs>
|
|
570
603
|
)
|
|
571
604
|
}
|
|
@@ -1,40 +1,33 @@
|
|
|
1
1
|
import { Users } from 'lucide-react'
|
|
2
|
-
import {
|
|
2
|
+
import { notFound } from 'next/navigation'
|
|
3
|
+
import { getMemberUsage } from '~/app/(app)/actions/members'
|
|
3
4
|
import { PageHeading } from '~/components/page-heading'
|
|
4
|
-
import { canManageOrganization } from '~/lib/app-navigation'
|
|
5
|
-
import {
|
|
6
|
-
getOrganizationMemberCount,
|
|
7
|
-
getUserMembershipCached
|
|
8
|
-
} from '~/lib/organization-access'
|
|
9
|
-
import { getOrganizationPlanCached } from '~/lib/organization-plan-limits'
|
|
10
5
|
import { MembersTabs } from './_components/members-tabs'
|
|
11
6
|
|
|
12
|
-
export default async function
|
|
7
|
+
export default async function MembersPage({
|
|
13
8
|
params
|
|
14
9
|
}: {
|
|
15
10
|
params: Promise<{ orgId: string }>
|
|
16
11
|
}) {
|
|
17
12
|
const { orgId } = await params
|
|
13
|
+
const usage = (await getMemberUsage(orgId))?.data
|
|
18
14
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
return redirect(`/app/${orgId}/dashboard`)
|
|
15
|
+
if (!usage) {
|
|
16
|
+
return notFound()
|
|
22
17
|
}
|
|
23
18
|
|
|
24
|
-
const [memberCount, { plan }] = await Promise.all([
|
|
25
|
-
getOrganizationMemberCount(orgId),
|
|
26
|
-
getOrganizationPlanCached(orgId)
|
|
27
|
-
])
|
|
28
|
-
|
|
29
19
|
return (
|
|
30
20
|
<div className="mx-auto w-full max-w-4xl space-y-6">
|
|
31
21
|
<PageHeading
|
|
32
22
|
title="Members"
|
|
33
|
-
description="Invite teammates,
|
|
23
|
+
description="Invite teammates, change roles, and review invitations."
|
|
34
24
|
icon={Users}
|
|
35
25
|
/>
|
|
36
26
|
|
|
37
|
-
<MembersTabs
|
|
27
|
+
<MembersTabs
|
|
28
|
+
memberCount={usage.memberCount}
|
|
29
|
+
maxMembers={usage.maxMembers}
|
|
30
|
+
/>
|
|
38
31
|
</div>
|
|
39
32
|
)
|
|
40
33
|
}
|
|
@@ -1,50 +1,36 @@
|
|
|
1
1
|
import { Settings } from 'lucide-react'
|
|
2
|
-
import {
|
|
2
|
+
import { notFound } from 'next/navigation'
|
|
3
|
+
import {
|
|
4
|
+
getOrganizationCached,
|
|
5
|
+
getTransferTargets
|
|
6
|
+
} from '~/app/(app)/actions/organizations'
|
|
3
7
|
import { PageHeading } from '~/components/page-heading'
|
|
4
8
|
import { Separator } from '~/components/ui/separator'
|
|
5
|
-
import { canManageOrganization } from '~/lib/app-navigation'
|
|
6
|
-
import {
|
|
7
|
-
getOrganizationMembers,
|
|
8
|
-
getUserMembershipCached,
|
|
9
|
-
getUserOrganizationsCached
|
|
10
|
-
} from '~/lib/organization-access'
|
|
11
|
-
import { resolveImageUrl } from '~/lib/storage'
|
|
12
9
|
import { DangerZone } from './_components/danger-zone'
|
|
13
10
|
import { GeneralSettingsForm } from './_components/general-settings-form'
|
|
14
11
|
|
|
15
|
-
export default async function
|
|
12
|
+
export default async function GeneralSettingsPage({
|
|
16
13
|
params
|
|
17
14
|
}: {
|
|
18
15
|
params: Promise<{ orgId: string }>
|
|
19
16
|
}) {
|
|
20
17
|
const { orgId } = await params
|
|
18
|
+
const org = (await getOrganizationCached(orgId))?.data
|
|
21
19
|
|
|
22
|
-
const membership = await getUserMembershipCached(orgId)
|
|
23
|
-
if (!membership || !canManageOrganization(membership.role)) {
|
|
24
|
-
return redirect(`/app/${orgId}/dashboard`)
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const org = (await getUserOrganizationsCached()).find(o => o.id === orgId)
|
|
28
20
|
if (!org) {
|
|
29
|
-
return
|
|
21
|
+
return notFound()
|
|
30
22
|
}
|
|
31
23
|
|
|
32
|
-
const
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
.map(m => ({
|
|
37
|
-
memberId: m.id,
|
|
38
|
-
name: m.user.name,
|
|
39
|
-
email: m.user.email,
|
|
40
|
-
role: m.role
|
|
41
|
-
}))
|
|
24
|
+
const isOwner = org.role === 'owner'
|
|
25
|
+
const transferTargets = isOwner
|
|
26
|
+
? ((await getTransferTargets(orgId))?.data ?? [])
|
|
27
|
+
: []
|
|
42
28
|
|
|
43
29
|
return (
|
|
44
30
|
<div className="mx-auto w-full max-w-3xl space-y-8">
|
|
45
31
|
<PageHeading
|
|
46
32
|
title="General"
|
|
47
|
-
description="
|
|
33
|
+
description="Change your organization's name, link, and logo."
|
|
48
34
|
icon={Settings}
|
|
49
35
|
/>
|
|
50
36
|
|
|
@@ -52,8 +38,8 @@ export default async function Page({
|
|
|
52
38
|
orgId={orgId}
|
|
53
39
|
defaultName={org.name}
|
|
54
40
|
defaultSlug={org.slug}
|
|
55
|
-
currentLogoUrl={resolveImageUrl(org.logo)}
|
|
56
41
|
currentLogoKey={org.logo}
|
|
42
|
+
currentLogoUrl={org.logoUrl}
|
|
57
43
|
/>
|
|
58
44
|
|
|
59
45
|
<Separator />
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { notFound } from 'next/navigation'
|
|
2
|
+
import { getOrganizationCached } from '~/app/(app)/actions/organizations'
|
|
3
|
+
import { SupportTicketConversation } from '~/components/support/support-ticket-conversation'
|
|
4
|
+
import { isManagerRole } from '~/lib/rbac'
|
|
5
|
+
|
|
6
|
+
export default async function OrganizationSupportTicketPage({
|
|
7
|
+
params
|
|
8
|
+
}: {
|
|
9
|
+
params: Promise<{ orgId: string; ticketId: string }>
|
|
10
|
+
}) {
|
|
11
|
+
const { orgId, ticketId } = await params
|
|
12
|
+
const org = (await getOrganizationCached(orgId))?.data
|
|
13
|
+
|
|
14
|
+
if (!org || !isManagerRole(org.role)) {
|
|
15
|
+
return notFound()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<SupportTicketConversation
|
|
20
|
+
mode="app"
|
|
21
|
+
ticketId={ticketId}
|
|
22
|
+
backHref={`/app/${orgId}/support-tickets`}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { notFound } from 'next/navigation'
|
|
2
|
+
import { getOrganizationCached } from '~/app/(app)/actions/organizations'
|
|
3
|
+
import { SupportTicketsTable } from '~/components/support/support-tickets-table'
|
|
4
|
+
import { isManagerRole } from '~/lib/rbac'
|
|
5
|
+
|
|
6
|
+
export default async function OrganizationSupportTicketsPage({
|
|
7
|
+
params
|
|
8
|
+
}: {
|
|
9
|
+
params: Promise<{ orgId: string }>
|
|
10
|
+
}) {
|
|
11
|
+
const { orgId } = await params
|
|
12
|
+
const org = (await getOrganizationCached(orgId))?.data
|
|
13
|
+
|
|
14
|
+
if (!org || !isManagerRole(org.role)) {
|
|
15
|
+
return notFound()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<SupportTicketsTable
|
|
20
|
+
mode="app"
|
|
21
|
+
organizationId={orgId}
|
|
22
|
+
basePath={`/app/${orgId}/support-tickets`}
|
|
23
|
+
/>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import { useParams } from 'next/navigation'
|
|
4
|
-
import { FC } from 'react'
|
|
5
3
|
import { DashboardHeader } from '~/components/dashboard-header'
|
|
6
|
-
import type { UserOrganization } from '~/lib/app-navigation'
|
|
7
4
|
import { AppSearch } from './app-search'
|
|
5
|
+
import { FeedbackDialog } from './feedback-dialog'
|
|
8
6
|
|
|
9
|
-
export
|
|
7
|
+
export function AppHeader({
|
|
8
|
+
user,
|
|
9
|
+
organizations,
|
|
10
|
+
isSuperAdmin,
|
|
11
|
+
isImpersonating
|
|
12
|
+
}: {
|
|
10
13
|
user: {
|
|
11
14
|
name?: string | null
|
|
12
15
|
email?: string | null
|
|
13
16
|
image?: string | null
|
|
14
17
|
}
|
|
15
|
-
organizations:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const role = organizations.find(o => o.id === orgId)?.role
|
|
20
|
-
|
|
18
|
+
organizations: { id: string; role: string }[]
|
|
19
|
+
isSuperAdmin: boolean
|
|
20
|
+
isImpersonating: boolean
|
|
21
|
+
}) {
|
|
21
22
|
return (
|
|
22
23
|
<DashboardHeader
|
|
23
24
|
user={user}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
25
|
+
isSuperAdmin={isSuperAdmin}
|
|
26
|
+
isImpersonating={isImpersonating}
|
|
27
|
+
search={<AppSearch organizations={organizations} />}
|
|
28
|
+
actions={<FeedbackDialog />}
|
|
27
29
|
/>
|
|
28
30
|
)
|
|
29
31
|
}
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { useParams } from 'next/navigation'
|
|
4
4
|
import { NavigationSearch } from '~/components/navigation-search'
|
|
5
5
|
import { getAppNavigation } from '~/lib/app-navigation'
|
|
6
6
|
|
|
7
|
-
export
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
export function AppSearch({
|
|
8
|
+
organizations
|
|
9
|
+
}: {
|
|
10
|
+
organizations: { id: string; role: string }[]
|
|
11
|
+
}) {
|
|
12
|
+
const params = useParams()
|
|
13
|
+
const orgId = params?.orgId as string | undefined
|
|
14
|
+
const role = organizations.find(o => o.id === orgId)?.role
|
|
15
|
+
|
|
16
|
+
if (!orgId || !role) {
|
|
17
|
+
return null
|
|
18
|
+
}
|
|
19
|
+
|
|
11
20
|
return (
|
|
12
21
|
<NavigationSearch
|
|
13
22
|
groups={getAppNavigation(orgId, role)}
|
|
14
|
-
dialogDescription="Search
|
|
23
|
+
dialogDescription="Search this organization's pages and settings."
|
|
15
24
|
/>
|
|
16
25
|
)
|
|
17
26
|
}
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
import { useParams } from 'next/navigation'
|
|
4
4
|
import { NavigationSidebar } from '~/components/navigation-sidebar'
|
|
5
|
-
import { getAppNavigation
|
|
6
|
-
import {
|
|
5
|
+
import { getAppNavigation } from '~/lib/app-navigation'
|
|
6
|
+
import {
|
|
7
|
+
OrganizationSwitcher,
|
|
8
|
+
type SwitcherOrganization
|
|
9
|
+
} from './organization-switcher'
|
|
7
10
|
|
|
8
11
|
export function AppSidebar({
|
|
9
12
|
organizations
|
|
10
13
|
}: {
|
|
11
|
-
organizations:
|
|
14
|
+
organizations: (SwitcherOrganization & { role: string })[]
|
|
12
15
|
}) {
|
|
13
16
|
const params = useParams()
|
|
14
17
|
const orgId = params?.orgId as string | undefined
|