From 84a2e55125c3c6b4ebb0c8cc4f2b76490ca67c4f Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Mon, 9 Sep 2024 01:15:03 -0400 Subject: [PATCH] chore: App router migration - general settings (#16312) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * added lockedSMS to future routes * add orgMigrations routes to future * correct metadata * small fix * add orgMigrations to future * Move components to client components * Remove tRPC element from edit user RSC * Get username for meta * Remove suspense query * Remove orgMigrations from app router * Type fix * Revert "Remove suspense query" This reverts commit eadd814f6e4a5d6856d9218342b7909c22fe62c6. * Handle suspenseQuery in app router * User edit page, fetch data server side * Update yarn.lock * Export getFixedT * Set PageWrapper as root layout for settings * Settings Layout accepts strings for shell heading * Add OOO to app router settings * Refactor layout for my-account pages * Remove instances of pages router from my-account * Refactor security pages * Add billing to app router * Add admin API link to layout * Add api keys page * Webhooks WIP * Refactor SettingsHeader to client component * Add webhook pages * Refactor API keys page * Add admin app page * Type fix * fix types * fix developer/webhooks/[id] param value type error * remove unnecessary code * do not pass t prop to CreateNewWebhookButton * fix type errors in webhook-edit-view * fix the remaining type errors * do not use prisma directly in generateMetadata * remove use client if unnecessary * Remove unused shell heading from SettingsLayoutAppDir * improve metadata * fix billing page * fix import in settings/teams * Use next `notFound()` * fix type check * fix type check * remove unused code * Fix calendar setting page * Separate settings pages into route groups * Refactor admin settings pages * Remove meta instance from billing page route * Update settings layoutAppDir * Refactor developer settings pages * Refactor out of office * Refactor my account settings * Refactor admin api page * Refactor security pages * Type fix * fix styling in settings layout --------- Co-authored-by: Somay Chauhan Co-authored-by: Omar López Co-authored-by: Benny Joo --- apps/web/app/_utils.tsx | 2 +- .../future/reschedule/[uid]/embed/page.tsx | 8 +- apps/web/app/future/reschedule/[uid]/page.tsx | 8 +- apps/web/app/future/routing-forms/page.tsx | 8 +- .../(admin)/admin/apps/[category]/page.tsx | 34 + .../admin/apps}/page.tsx | 6 +- .../settings/(admin)/admin/flags/page.tsx | 20 + .../(admin)/admin/impersonation/page.tsx | 27 + .../settings/(admin)/admin/lockedSMS/page.tsx | 21 + .../settings/(admin)/admin/oAuth/page.tsx | 21 + .../admin/organizations/[id]/edit/page.tsx | 52 ++ .../(admin)/admin/organizations/page.tsx | 29 + .../settings/{ => (admin)}/admin/page.tsx | 0 .../(admin)/admin/users/[id]/edit/page.tsx | 52 ++ .../settings/(admin)/admin/users/add/page.tsx | 20 + .../settings/(admin)/admin/users/page.tsx | 30 + .../{admin/apps => (admin)}/layout.tsx | 0 .../settings/(settings)/billing/page.tsx | 31 + .../(settings)/developer/api-keys/page.tsx | 33 + .../developer/webhooks/[id]/page.tsx | 36 + .../developer/webhooks/new/page.tsx | 31 + .../(settings)/developer/webhooks/page.tsx | 16 + .../app/future/settings/(settings)/layout.tsx | 5 + .../(settings)/my-account/appearance/page.tsx | 27 + .../(settings)/my-account/calendars/page.tsx | 41 + .../my-account/conferencing/page.tsx | 40 + .../(settings)/my-account/general/page.tsx | 27 + .../my-account/out-of-office/page.tsx | 31 + .../(settings)/my-account/profile/page.tsx | 31 + .../organizations/[id]/about/page.tsx | 0 .../organizations/[id]/add-teams/page.tsx | 0 .../[id]/onboard-admins/page.tsx | 6 +- .../organizations/admin-api/page.tsx | 29 + .../organizations}/appearance/layout.tsx | 2 +- .../organizations/appearance/page.tsx | 0 .../organizations/billing}/layout.tsx | 2 +- .../organizations/billing/page.tsx | 3 +- .../organizations/general}/layout.tsx | 2 +- .../organizations/general/page.tsx | 0 .../organizations/members}/layout.tsx | 2 +- .../organizations/members/page.tsx | 0 .../organizations/new/page.tsx | 0 .../organizations/profile/layout.tsx | 5 + .../organizations/profile/page.tsx | 0 .../teams/other/[id]/appearance/layout.tsx | 5 + .../teams/other/[id]/appearance/page.tsx | 0 .../teams/other/[id]/members/layout.tsx | 5 + .../teams/other/[id]/members/page.tsx | 0 .../teams/other/[id]/profile/layout.tsx | 5 + .../teams/other/[id]/profile/page.tsx | 0 .../organizations/teams/other/layout.tsx | 5 + .../organizations/teams/other/page.ts | 0 .../security/impersonation/page.tsx | 31 + .../(settings)/security/password/page.tsx | 28 + .../settings/(settings)/security/sso/page.tsx | 30 + .../security/two-factor-auth/page.tsx | 30 + .../teams/[id]/appearance/page.tsx | 0 .../teams/[id]/billing/page.tsx | 3 +- .../teams/[id]/event-type/page.tsx | 0 .../teams/[id]/members/page.tsx | 0 .../teams/[id]/onboard-members/page.tsx | 0 .../teams/[id]/profile/page.tsx | 0 .../flags => (settings)/teams}/layout.tsx | 2 +- .../{ => (settings)}/teams/new/page.tsx | 0 .../settings/{ => (settings)}/teams/page.ts | 0 .../app/future/settings/admin/apps/page.tsx | 10 - .../app/future/settings/admin/flags/page.tsx | 10 - .../settings/admin/impersonation/layout.tsx | 5 - .../settings/admin/impersonation/page.tsx | 10 - .../settings/admin/oAuth/oAuthView/layout.tsx | 3 - .../settings/admin/oAuth/oAuthView/page.tsx | 10 - .../app/future/settings/admin/oAuth/page.tsx | 13 - .../settings/admin/organizations/layout.tsx | 5 - .../settings/admin/organizations/page.tsx | 11 - .../settings/admin/users/[id]/edit/page.tsx | 32 - .../future/settings/admin/users/add/page.tsx | 13 - .../app/future/settings/admin/users/page.tsx | 13 - .../settings/my-account/appearance/page.tsx | 10 - .../settings/my-account/calendars/page.tsx | 10 - .../settings/my-account/conferencing/page.tsx | 10 - .../settings/my-account/general/page.tsx | 10 - .../settings/my-account/profile/layout.tsx | 5 - .../settings/my-account/profile/page.tsx | 10 - .../organizations/appearance/layout.tsx | 5 - .../settings/organizations/billing/layout.tsx | 5 - .../settings/organizations/general/layout.tsx | 5 - .../settings/organizations/members/layout.tsx | 5 - .../settings/organizations/profile/layout.tsx | 5 - .../teams/other/[id]/appearance/layout.tsx | 5 - .../teams/other/[id]/members/layout.tsx | 5 - .../teams/other/[id]/profile/layout.tsx | 5 - .../organizations/teams/other/layout.tsx | 5 - .../security/impersonation/layout.tsx | 5 - .../settings/security/impersonation/page.tsx | 10 - .../settings/security/password/layout.tsx | 5 - .../settings/security/password/page.tsx | 10 - .../future/settings/security/sso/layout.tsx | 5 - .../app/future/settings/security/sso/page.tsx | 11 - .../security/two-factor-auth/layout.tsx | 5 - .../security/two-factor-auth/page.tsx | 10 - apps/web/app/future/settings/teams/layout.tsx | 5 - .../components/apps/CalendarListContainer.tsx | 4 +- .../components/auth/layouts/AdminLayout.tsx | 2 + .../auth/layouts/AdminLayoutAppDir.tsx | 2 +- .../settings/CustomEmailTextField.tsx | 3 +- .../settings/TravelScheduleModal.tsx | 3 +- .../moveTeamToOrg/getServerSideProps.tsx | 36 + .../moveUserToOrg/getServerSideProps.tsx | 32 + .../removeTeamFromOrg/getServerSideProps.tsx | 32 + .../removeUserFromOrg/getServerSideProps.tsx | 32 + .../settings/admin/components}/UsersTable.tsx | 0 .../settings/admin/impersonation-view.tsx | 57 ++ .../settings/admin/locked-sms-view.tsx} | 6 +- .../settings/admin/oauth-view.tsx} | 3 +- .../modules/settings/billing/billing-view.tsx | 64 ++ .../settings/developer/api-keys-view.tsx | 108 +++ .../components/CreateApiKeyButton.tsx | 38 + .../settings/my-account/appearance-view.tsx | 440 +++++++++++ .../components/AddCalendarButton.tsx | 18 + .../settings/my-account/conferencing-view.tsx | 101 +++ .../settings/my-account/general-view.tsx | 378 +++++++++ .../settings/my-account/profile-view.tsx | 730 +++++++++++++++++ .../settings/security/impersonation-view.tsx | 77 ++ .../settings/security/password-view.tsx | 315 ++++++++ .../security/two-factor-auth-view.tsx | 86 ++ apps/web/pages/settings/admin/flags.tsx | 8 +- .../pages/settings/admin/impersonation.tsx | 2 - .../pages/settings/admin/lockedSMS/index.tsx | 11 +- apps/web/pages/settings/admin/oAuth/index.tsx | 11 +- .../admin/orgMigrations/moveTeamToOrg.tsx | 41 +- .../admin/orgMigrations/moveUserToOrg.tsx | 37 +- .../admin/orgMigrations/removeTeamFromOrg.tsx | 37 +- .../admin/orgMigrations/removeUserFromOrg.tsx | 42 +- .../admin/organizations/[id]/edit.tsx | 37 +- .../settings/admin/organizations/index.tsx | 23 +- .../pages/settings/admin/users/[id]/edit.tsx | 36 +- apps/web/pages/settings/admin/users/add.tsx | 11 +- apps/web/pages/settings/admin/users/index.tsx | 23 +- apps/web/pages/settings/billing/index.tsx | 62 +- .../web/pages/settings/developer/api-keys.tsx | 105 +-- .../developer/webhooks/[id]/index.tsx | 40 +- .../settings/developer/webhooks/index.tsx | 33 +- .../pages/settings/developer/webhooks/new.tsx | 22 +- .../pages/settings/my-account/appearance.tsx | 447 +---------- .../pages/settings/my-account/calendars.tsx | 41 +- .../settings/my-account/conferencing.tsx | 125 +-- .../web/pages/settings/my-account/general.tsx | 384 +-------- .../my-account/out-of-office/index.tsx | 70 +- .../web/pages/settings/my-account/profile.tsx | 733 +----------------- .../settings/organizations/admin-api.tsx | 20 +- .../pages/settings/security/impersonation.tsx | 80 +- apps/web/pages/settings/security/password.tsx | 316 +------- apps/web/pages/settings/security/sso.tsx | 20 +- .../settings/security/two-factor-auth.tsx | 94 +-- packages/features/apps/AdminAppsList.tsx | 2 + .../ee/common/components/LicenseRequired.tsx | 10 +- .../pages/settings/admin-api.tsx | 14 +- .../pages/settings/admin/AdminOrgEditPage.tsx | 44 +- .../pages/settings/admin/AdminOrgPage.tsx | 41 +- .../features/ee/sso/page/user-sso-view.tsx | 9 - .../ee/users/components/UsersTable.tsx | 4 +- .../ee/users/pages/users-add-view.tsx | 28 +- .../ee/users/pages/users-edit-view.tsx | 60 +- .../ee/users/pages/users-listing-view.tsx | 12 - .../flags/pages/flag-listing-view.tsx | 17 +- .../settings/appDir/SettingsHeader.tsx | 57 ++ .../SettingsLayoutAppDir.tsx | 501 ++++++------ .../settings/layouts/SettingsLayout.tsx | 2 + .../CreateNewOutOfOfficeEntryButton.tsx | 46 ++ .../CreateOrEditOutOfOfficeModal.tsx | 10 +- .../OutOfOfficeEntriesList.tsx | 33 +- .../components/CreateNewWebhookButton.tsx | 31 + .../features/webhooks/components/index.ts | 1 + .../features/webhooks/lib/getNewWebhookUrl.ts | 9 + .../webhooks/pages/webhook-edit-view.tsx | 52 +- .../webhooks/pages/webhook-new-view.tsx | 25 +- .../features/webhooks/pages/webhooks-view.tsx | 94 +-- .../lib/server/repository/organization.ts | 47 ++ packages/lib/server/repository/user.ts | 8 + packages/lib/server/repository/webhook.ts | 180 +++++ .../DestinationCalendarSelector.tsx | 2 + .../viewer/organizations/adminGet.handler.ts | 54 +- .../routers/viewer/webhook/get.handler.ts | 19 +- .../viewer/webhook/getByViewer.handler.ts | 135 +--- .../CreateButtonWithTeamsList.tsx | 2 + packages/ui/components/list/List.tsx | 2 +- packages/ui/components/tooltip/Tooltip.tsx | 2 + 187 files changed, 4628 insertions(+), 3586 deletions(-) create mode 100644 apps/web/app/future/settings/(admin)/admin/apps/[category]/page.tsx rename apps/web/app/future/settings/{admin/apps/[category] => (admin)/admin/apps}/page.tsx (68%) create mode 100644 apps/web/app/future/settings/(admin)/admin/flags/page.tsx create mode 100644 apps/web/app/future/settings/(admin)/admin/impersonation/page.tsx create mode 100644 apps/web/app/future/settings/(admin)/admin/lockedSMS/page.tsx create mode 100644 apps/web/app/future/settings/(admin)/admin/oAuth/page.tsx create mode 100644 apps/web/app/future/settings/(admin)/admin/organizations/[id]/edit/page.tsx create mode 100644 apps/web/app/future/settings/(admin)/admin/organizations/page.tsx rename apps/web/app/future/settings/{ => (admin)}/admin/page.tsx (100%) create mode 100644 apps/web/app/future/settings/(admin)/admin/users/[id]/edit/page.tsx create mode 100644 apps/web/app/future/settings/(admin)/admin/users/add/page.tsx create mode 100644 apps/web/app/future/settings/(admin)/admin/users/page.tsx rename apps/web/app/future/settings/{admin/apps => (admin)}/layout.tsx (100%) create mode 100644 apps/web/app/future/settings/(settings)/billing/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/developer/api-keys/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/developer/webhooks/[id]/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/developer/webhooks/new/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/developer/webhooks/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/layout.tsx create mode 100644 apps/web/app/future/settings/(settings)/my-account/appearance/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/my-account/calendars/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/my-account/conferencing/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/my-account/general/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/my-account/out-of-office/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/my-account/profile/page.tsx rename apps/web/app/future/settings/{ => (settings)}/organizations/[id]/about/page.tsx (100%) rename apps/web/app/future/settings/{ => (settings)}/organizations/[id]/add-teams/page.tsx (100%) rename apps/web/app/future/settings/{ => (settings)}/organizations/[id]/onboard-admins/page.tsx (89%) create mode 100644 apps/web/app/future/settings/(settings)/organizations/admin-api/page.tsx rename apps/web/app/future/settings/{my-account => (settings)/organizations}/appearance/layout.tsx (51%) rename apps/web/app/future/settings/{ => (settings)}/organizations/appearance/page.tsx (100%) rename apps/web/app/future/settings/{my-account/calendars => (settings)/organizations/billing}/layout.tsx (51%) rename apps/web/app/future/settings/{ => (settings)}/organizations/billing/page.tsx (80%) rename apps/web/app/future/settings/{my-account/conferencing => (settings)/organizations/general}/layout.tsx (51%) rename apps/web/app/future/settings/{ => (settings)}/organizations/general/page.tsx (100%) rename apps/web/app/future/settings/{my-account/general => (settings)/organizations/members}/layout.tsx (51%) rename apps/web/app/future/settings/{ => (settings)}/organizations/members/page.tsx (100%) rename apps/web/app/future/settings/{ => (settings)}/organizations/new/page.tsx (100%) create mode 100644 apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx rename apps/web/app/future/settings/{ => (settings)}/organizations/profile/page.tsx (100%) create mode 100644 apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx rename apps/web/app/future/settings/{ => (settings)}/organizations/teams/other/[id]/appearance/page.tsx (100%) create mode 100644 apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx rename apps/web/app/future/settings/{ => (settings)}/organizations/teams/other/[id]/members/page.tsx (100%) create mode 100644 apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx rename apps/web/app/future/settings/{ => (settings)}/organizations/teams/other/[id]/profile/page.tsx (100%) create mode 100644 apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx rename apps/web/app/future/settings/{ => (settings)}/organizations/teams/other/page.ts (100%) create mode 100644 apps/web/app/future/settings/(settings)/security/impersonation/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/security/password/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/security/sso/page.tsx create mode 100644 apps/web/app/future/settings/(settings)/security/two-factor-auth/page.tsx rename apps/web/app/future/settings/{ => (settings)}/teams/[id]/appearance/page.tsx (100%) rename apps/web/app/future/settings/{ => (settings)}/teams/[id]/billing/page.tsx (80%) rename apps/web/app/future/settings/{ => (settings)}/teams/[id]/event-type/page.tsx (100%) rename apps/web/app/future/settings/{ => (settings)}/teams/[id]/members/page.tsx (100%) rename apps/web/app/future/settings/{ => (settings)}/teams/[id]/onboard-members/page.tsx (100%) rename apps/web/app/future/settings/{ => (settings)}/teams/[id]/profile/page.tsx (100%) rename apps/web/app/future/settings/{admin/flags => (settings)/teams}/layout.tsx (52%) rename apps/web/app/future/settings/{ => (settings)}/teams/new/page.tsx (100%) rename apps/web/app/future/settings/{ => (settings)}/teams/page.ts (100%) delete mode 100644 apps/web/app/future/settings/admin/apps/page.tsx delete mode 100644 apps/web/app/future/settings/admin/flags/page.tsx delete mode 100644 apps/web/app/future/settings/admin/impersonation/layout.tsx delete mode 100644 apps/web/app/future/settings/admin/impersonation/page.tsx delete mode 100644 apps/web/app/future/settings/admin/oAuth/oAuthView/layout.tsx delete mode 100644 apps/web/app/future/settings/admin/oAuth/oAuthView/page.tsx delete mode 100644 apps/web/app/future/settings/admin/oAuth/page.tsx delete mode 100644 apps/web/app/future/settings/admin/organizations/layout.tsx delete mode 100644 apps/web/app/future/settings/admin/organizations/page.tsx delete mode 100644 apps/web/app/future/settings/admin/users/[id]/edit/page.tsx delete mode 100644 apps/web/app/future/settings/admin/users/add/page.tsx delete mode 100644 apps/web/app/future/settings/admin/users/page.tsx delete mode 100644 apps/web/app/future/settings/my-account/appearance/page.tsx delete mode 100644 apps/web/app/future/settings/my-account/calendars/page.tsx delete mode 100644 apps/web/app/future/settings/my-account/conferencing/page.tsx delete mode 100644 apps/web/app/future/settings/my-account/general/page.tsx delete mode 100644 apps/web/app/future/settings/my-account/profile/layout.tsx delete mode 100644 apps/web/app/future/settings/my-account/profile/page.tsx delete mode 100644 apps/web/app/future/settings/organizations/appearance/layout.tsx delete mode 100644 apps/web/app/future/settings/organizations/billing/layout.tsx delete mode 100644 apps/web/app/future/settings/organizations/general/layout.tsx delete mode 100644 apps/web/app/future/settings/organizations/members/layout.tsx delete mode 100644 apps/web/app/future/settings/organizations/profile/layout.tsx delete mode 100644 apps/web/app/future/settings/organizations/teams/other/[id]/appearance/layout.tsx delete mode 100644 apps/web/app/future/settings/organizations/teams/other/[id]/members/layout.tsx delete mode 100644 apps/web/app/future/settings/organizations/teams/other/[id]/profile/layout.tsx delete mode 100644 apps/web/app/future/settings/organizations/teams/other/layout.tsx delete mode 100644 apps/web/app/future/settings/security/impersonation/layout.tsx delete mode 100644 apps/web/app/future/settings/security/impersonation/page.tsx delete mode 100644 apps/web/app/future/settings/security/password/layout.tsx delete mode 100644 apps/web/app/future/settings/security/password/page.tsx delete mode 100644 apps/web/app/future/settings/security/sso/layout.tsx delete mode 100644 apps/web/app/future/settings/security/sso/page.tsx delete mode 100644 apps/web/app/future/settings/security/two-factor-auth/layout.tsx delete mode 100644 apps/web/app/future/settings/security/two-factor-auth/page.tsx delete mode 100644 apps/web/app/future/settings/teams/layout.tsx create mode 100644 apps/web/lib/settings/admin/orgMigrations/moveTeamToOrg/getServerSideProps.tsx create mode 100644 apps/web/lib/settings/admin/orgMigrations/moveUserToOrg/getServerSideProps.tsx create mode 100644 apps/web/lib/settings/admin/orgMigrations/removeTeamFromOrg/getServerSideProps.tsx create mode 100644 apps/web/lib/settings/admin/orgMigrations/removeUserFromOrg/getServerSideProps.tsx rename apps/web/{pages/settings/admin/lockedSMS => modules/settings/admin/components}/UsersTable.tsx (100%) create mode 100644 apps/web/modules/settings/admin/impersonation-view.tsx rename apps/web/{pages/settings/admin/lockedSMS/lockedSMSView.tsx => modules/settings/admin/locked-sms-view.tsx} (91%) rename apps/web/{pages/settings/admin/oAuth/oAuthView.tsx => modules/settings/admin/oauth-view.tsx} (96%) create mode 100644 apps/web/modules/settings/billing/billing-view.tsx create mode 100644 apps/web/modules/settings/developer/api-keys-view.tsx create mode 100644 apps/web/modules/settings/developer/components/CreateApiKeyButton.tsx create mode 100644 apps/web/modules/settings/my-account/appearance-view.tsx create mode 100644 apps/web/modules/settings/my-account/components/AddCalendarButton.tsx create mode 100644 apps/web/modules/settings/my-account/conferencing-view.tsx create mode 100644 apps/web/modules/settings/my-account/general-view.tsx create mode 100644 apps/web/modules/settings/my-account/profile-view.tsx create mode 100644 apps/web/modules/settings/security/impersonation-view.tsx create mode 100644 apps/web/modules/settings/security/password-view.tsx create mode 100644 apps/web/modules/settings/security/two-factor-auth-view.tsx create mode 100644 packages/features/settings/appDir/SettingsHeader.tsx rename packages/features/settings/{layouts => appDir}/SettingsLayoutAppDir.tsx (62%) create mode 100644 packages/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton.tsx rename packages/features/settings/{ => outOfOffice}/CreateOrEditOutOfOfficeModal.tsx (97%) rename packages/features/settings/{ => outOfOffice}/OutOfOfficeEntriesList.tsx (88%) create mode 100644 packages/features/webhooks/components/CreateNewWebhookButton.tsx create mode 100644 packages/features/webhooks/lib/getNewWebhookUrl.ts create mode 100644 packages/lib/server/repository/webhook.ts diff --git a/apps/web/app/_utils.tsx b/apps/web/app/_utils.tsx index a4bcdf1d10..21ddec0994 100644 --- a/apps/web/app/_utils.tsx +++ b/apps/web/app/_utils.tsx @@ -22,7 +22,7 @@ const create = async (locale: string, ns: string) => { return _i18n; }; -const getFixedT = async (locale: string, ns: string) => { +export const getFixedT = async (locale: string, ns = "common") => { const i18n = await create(locale, ns); return i18n.getFixedT(locale, ns); diff --git a/apps/web/app/future/reschedule/[uid]/embed/page.tsx b/apps/web/app/future/reschedule/[uid]/embed/page.tsx index a81eed6523..4cc22aabf2 100644 --- a/apps/web/app/future/reschedule/[uid]/embed/page.tsx +++ b/apps/web/app/future/reschedule/[uid]/embed/page.tsx @@ -1,17 +1,11 @@ import { getServerSideProps } from "@pages/reschedule/[uid]"; import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { SearchParams } from "app/_types"; -import type { Params } from "next/dist/shared/lib/router/utils/route-matcher"; +import type { PageProps } from "app/_types"; import { cookies, headers } from "next/headers"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; import withEmbedSsr from "@lib/withEmbedSsr"; -type PageProps = Readonly<{ - params: Params; - searchParams: SearchParams; -}>; - const Page = async ({ params, searchParams }: PageProps) => { const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); await withAppDirSsr(withEmbedSsr(getServerSideProps))(legacyCtx); diff --git a/apps/web/app/future/reschedule/[uid]/page.tsx b/apps/web/app/future/reschedule/[uid]/page.tsx index af6ffb2505..921369891f 100644 --- a/apps/web/app/future/reschedule/[uid]/page.tsx +++ b/apps/web/app/future/reschedule/[uid]/page.tsx @@ -1,8 +1,7 @@ import { getServerSideProps } from "@pages/reschedule/[uid]"; import { withAppDirSsr } from "app/WithAppDirSsr"; -import type { SearchParams } from "app/_types"; +import type { PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; -import type { Params } from "next/dist/shared/lib/router/utils/route-matcher"; import { headers, cookies } from "next/headers"; import { buildLegacyCtx } from "@lib/buildLegacyCtx"; @@ -13,11 +12,6 @@ export const generateMetadata = async () => () => "" ); -type PageProps = Readonly<{ - params: Params; - searchParams: SearchParams; -}>; - const getData = withAppDirSsr(getServerSideProps); const Page = async ({ params, searchParams }: PageProps) => { diff --git a/apps/web/app/future/routing-forms/page.tsx b/apps/web/app/future/routing-forms/page.tsx index 52375eb677..f904f3eaf4 100644 --- a/apps/web/app/future/routing-forms/page.tsx +++ b/apps/web/app/future/routing-forms/page.tsx @@ -1,6 +1,5 @@ -import { type SearchParams } from "app/_types"; +import type { PageProps } from "app/_types"; import { type GetServerSidePropsContext } from "next"; -import type { Params } from "next/dist/shared/lib/router/utils/route-matcher"; import { cookies, headers } from "next/headers"; import { redirect } from "next/navigation"; import z from "zod"; @@ -21,11 +20,6 @@ const getPageProps = async (context: GetServerSidePropsContext) => { return redirect(`/apps/routing-forms/${pages.length ? pages.join("/") : ""}`); }; -type PageProps = Readonly<{ - params: Params; - searchParams: SearchParams; -}>; - const Page = async ({ params, searchParams }: PageProps) => { const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams); await getPageProps(legacyCtx); diff --git a/apps/web/app/future/settings/(admin)/admin/apps/[category]/page.tsx b/apps/web/app/future/settings/(admin)/admin/apps/[category]/page.tsx new file mode 100644 index 0000000000..90c04a5c3a --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/apps/[category]/page.tsx @@ -0,0 +1,34 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import AdminAppsList from "@calcom/features/apps/AdminAppsList"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("apps"), + (t) => t("admin_apps_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + +
+ +
+
+ ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/admin/apps/[category]/page.tsx b/apps/web/app/future/settings/(admin)/admin/apps/page.tsx similarity index 68% rename from apps/web/app/future/settings/admin/apps/[category]/page.tsx rename to apps/web/app/future/settings/(admin)/admin/apps/page.tsx index 0805169a9e..116b5da1ef 100644 --- a/apps/web/app/future/settings/admin/apps/[category]/page.tsx +++ b/apps/web/app/future/settings/(admin)/admin/apps/page.tsx @@ -1,5 +1,5 @@ -import Page from "@pages/settings/admin/apps/[category]"; import { _generateMetadata } from "app/_utils"; +import { redirect } from "next/navigation"; export const generateMetadata = async () => await _generateMetadata( @@ -7,4 +7,8 @@ export const generateMetadata = async () => (t) => t("admin_apps_description") ); +const Page = () => { + redirect("/[category]"); +}; + export default Page; diff --git a/apps/web/app/future/settings/(admin)/admin/flags/page.tsx b/apps/web/app/future/settings/(admin)/admin/flags/page.tsx new file mode 100644 index 0000000000..9322b5fd8e --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/flags/page.tsx @@ -0,0 +1,20 @@ +import { _generateMetadata } from "app/_utils"; + +import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "Feature Flags", + () => "Here you can toggle your Cal.com instance features." + ); + +const Page = async () => { + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(admin)/admin/impersonation/page.tsx b/apps/web/app/future/settings/(admin)/admin/impersonation/page.tsx new file mode 100644 index 0000000000..d3c29d61ad --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/impersonation/page.tsx @@ -0,0 +1,27 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import ImpersonationView from "~/settings/admin/impersonation-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("admin"), + (t) => t("impersonation") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(admin)/admin/lockedSMS/page.tsx b/apps/web/app/future/settings/(admin)/admin/lockedSMS/page.tsx new file mode 100644 index 0000000000..9be28b5a4c --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/lockedSMS/page.tsx @@ -0,0 +1,21 @@ +import { _generateMetadata } from "app/_utils"; + +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import LockedSMSView from "~/settings/admin/locked-sms-view"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "Locked SMS", + () => "Lock or unlock SMS sending for users" + ); + +const Page = () => { + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(admin)/admin/oAuth/page.tsx b/apps/web/app/future/settings/(admin)/admin/oAuth/page.tsx new file mode 100644 index 0000000000..158965294d --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/oAuth/page.tsx @@ -0,0 +1,21 @@ +import { _generateMetadata } from "app/_utils"; + +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import LegacyPage from "~/settings/admin/oauth-view"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "OAuth", + () => "Add new OAuth Clients" + ); + +const Page = () => { + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(admin)/admin/organizations/[id]/edit/page.tsx b/apps/web/app/future/settings/(admin)/admin/organizations/[id]/edit/page.tsx new file mode 100644 index 0000000000..1e94efb817 --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/organizations/[id]/edit/page.tsx @@ -0,0 +1,52 @@ +import { type Params } from "app/_types"; +import { _generateMetadata } from "app/_utils"; +import { notFound } from "next/navigation"; +import { z } from "zod"; + +import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; +import { OrgForm } from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgEditPage"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { OrganizationRepository } from "@calcom/lib/server/repository/organization"; + +const orgIdSchema = z.object({ id: z.coerce.number() }); + +export const generateMetadata = async ({ params }: { params: Params }) => { + const input = orgIdSchema.safeParse(params); + if (!input.success) { + return await _generateMetadata( + () => `Editing organization`, + () => "Here you can edit an organization." + ); + } + + const org = await OrganizationRepository.adminFindById({ id: input.data.id }); + + return await _generateMetadata( + () => `Editing organization: ${org.name}`, + () => "Here you can edit an organization." + ); +}; + +const Page = async ({ params }: { params: Params }) => { + const input = orgIdSchema.safeParse(params); + + if (!input.success) notFound(); + + try { + const org = await OrganizationRepository.adminFindById({ id: input.data.id }); + + return ( + + + + + + ); + } catch { + notFound(); + } +}; + +export default Page; diff --git a/apps/web/app/future/settings/(admin)/admin/organizations/page.tsx b/apps/web/app/future/settings/(admin)/admin/organizations/page.tsx new file mode 100644 index 0000000000..672632f91a --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/organizations/page.tsx @@ -0,0 +1,29 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; +import AdminOrgTable from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("organizations"), + (t) => t("orgs_page_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + return ( + + + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/admin/page.tsx b/apps/web/app/future/settings/(admin)/admin/page.tsx similarity index 100% rename from apps/web/app/future/settings/admin/page.tsx rename to apps/web/app/future/settings/(admin)/admin/page.tsx diff --git a/apps/web/app/future/settings/(admin)/admin/users/[id]/edit/page.tsx b/apps/web/app/future/settings/(admin)/admin/users/[id]/edit/page.tsx new file mode 100644 index 0000000000..6e554b1973 --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/users/[id]/edit/page.tsx @@ -0,0 +1,52 @@ +import { type Params } from "app/_types"; +import { _generateMetadata } from "app/_utils"; +import { notFound } from "next/navigation"; +import { z } from "zod"; + +import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; +import { UsersEditView } from "@calcom/features/ee/users/pages/users-edit-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { UserRepository } from "@calcom/lib/server/repository/user"; + +const userIdSchema = z.object({ id: z.coerce.number() }); + +export const generateMetadata = async ({ params }: { params: Params }) => { + const input = userIdSchema.safeParse(params); + if (!input.success) { + return await _generateMetadata( + () => `Editing user`, + () => "Here you can edit a current user." + ); + } + + const user = await UserRepository.adminFindById(input.data.id); + + return await _generateMetadata( + () => `Editing user: ${user.username}`, + () => "Here you can edit a current user." + ); +}; + +const Page = async ({ params }: { params: Params }) => { + const input = userIdSchema.safeParse(params); + + if (!input.success) { + notFound(); + } + + try { + const user = await UserRepository.adminFindById(input.data.id); + + return ( + + + + + + ); + } catch { + notFound(); + } +}; + +export default Page; diff --git a/apps/web/app/future/settings/(admin)/admin/users/add/page.tsx b/apps/web/app/future/settings/(admin)/admin/users/add/page.tsx new file mode 100644 index 0000000000..ee1aeb04c0 --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/users/add/page.tsx @@ -0,0 +1,20 @@ +import { _generateMetadata } from "app/_utils"; + +import UsersAddView from "@calcom/features/ee/users/pages/users-add-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "Add new user", + () => "Here you can add a new user." + ); + +const Page = () => { + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(admin)/admin/users/page.tsx b/apps/web/app/future/settings/(admin)/admin/users/page.tsx new file mode 100644 index 0000000000..26fb4f30d1 --- /dev/null +++ b/apps/web/app/future/settings/(admin)/admin/users/page.tsx @@ -0,0 +1,30 @@ +import { _generateMetadata } from "app/_utils"; + +import UsersListingView from "@calcom/features/ee/users/pages/users-listing-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { Button } from "@calcom/ui"; + +export const generateMetadata = async () => + await _generateMetadata( + () => "Users", + () => "A list of all the users in your account including their name, title, email and role." + ); + +const Page = () => { + return ( + + {/* TODO: Add import users functionality */} + {/* */} + + + }> + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/admin/apps/layout.tsx b/apps/web/app/future/settings/(admin)/layout.tsx similarity index 100% rename from apps/web/app/future/settings/admin/apps/layout.tsx rename to apps/web/app/future/settings/(admin)/layout.tsx diff --git a/apps/web/app/future/settings/(settings)/billing/page.tsx b/apps/web/app/future/settings/(settings)/billing/page.tsx new file mode 100644 index 0000000000..656487851d --- /dev/null +++ b/apps/web/app/future/settings/(settings)/billing/page.tsx @@ -0,0 +1,31 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import BillingView from "~/settings/billing/billing-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("billing"), + (t) => t("manage_billing_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/developer/api-keys/page.tsx b/apps/web/app/future/settings/(settings)/developer/api-keys/page.tsx new file mode 100644 index 0000000000..31f740e36d --- /dev/null +++ b/apps/web/app/future/settings/(settings)/developer/api-keys/page.tsx @@ -0,0 +1,33 @@ +import { getFixedT, _generateMetadata } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { APP_NAME } from "@calcom/lib/constants"; + +import ApiKeysView from "~/settings/developer/api-keys-view"; +import NewApiKeyButton from "~/settings/developer/components/CreateApiKeyButton"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("api_keys"), + (t) => t("create_first_api_key_description", { appName: APP_NAME }) + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + } + borderInShellHeader={true}> + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/developer/webhooks/[id]/page.tsx b/apps/web/app/future/settings/(settings)/developer/webhooks/[id]/page.tsx new file mode 100644 index 0000000000..709d2e0fff --- /dev/null +++ b/apps/web/app/future/settings/(settings)/developer/webhooks/[id]/page.tsx @@ -0,0 +1,36 @@ +import type { PageProps } from "app/_types"; +import { getFixedT, _generateMetadata } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { EditWebhookView } from "@calcom/features/webhooks/pages/webhook-edit-view"; +import { APP_NAME } from "@calcom/lib/constants"; +import { WebhookRepository } from "@calcom/lib/server/repository/webhook"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("webhooks"), + (t) => t("add_webhook_description", { appName: APP_NAME }) + ); + +const Page = async ({ params }: PageProps) => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + const id = typeof params?.id === "string" ? params.id : undefined; + + const webhook = await WebhookRepository.findByWebhookId(id); + console.log("This is the webhook", webhook); + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/developer/webhooks/new/page.tsx b/apps/web/app/future/settings/(settings)/developer/webhooks/new/page.tsx new file mode 100644 index 0000000000..4c60a8989f --- /dev/null +++ b/apps/web/app/future/settings/(settings)/developer/webhooks/new/page.tsx @@ -0,0 +1,31 @@ +import { getFixedT, _generateMetadata } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { NewWebhookView } from "@calcom/features/webhooks/pages/webhook-new-view"; +import { APP_NAME } from "@calcom/lib/constants"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("webhooks"), + (t) => t("add_webhook_description", { appName: APP_NAME }) + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/developer/webhooks/page.tsx b/apps/web/app/future/settings/(settings)/developer/webhooks/page.tsx new file mode 100644 index 0000000000..406599b63a --- /dev/null +++ b/apps/web/app/future/settings/(settings)/developer/webhooks/page.tsx @@ -0,0 +1,16 @@ +import { _generateMetadata } from "app/_utils"; + +import { WebhooksViewAppDir } from "@calcom/features/webhooks/pages/webhooks-view"; +import { APP_NAME } from "@calcom/lib/constants"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("webhooks"), + (t) => t("add_webhook_description", { appName: APP_NAME }) + ); + +const Page = () => { + return ; +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/layout.tsx b/apps/web/app/future/settings/(settings)/layout.tsx new file mode 100644 index 0000000000..3fc739975d --- /dev/null +++ b/apps/web/app/future/settings/(settings)/layout.tsx @@ -0,0 +1,5 @@ +import { WithLayout } from "app/layoutHOC"; + +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; + +export default WithLayout({ getLayout: getLayout })<"L">; diff --git a/apps/web/app/future/settings/(settings)/my-account/appearance/page.tsx b/apps/web/app/future/settings/(settings)/my-account/appearance/page.tsx new file mode 100644 index 0000000000..b32f763a79 --- /dev/null +++ b/apps/web/app/future/settings/(settings)/my-account/appearance/page.tsx @@ -0,0 +1,27 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import AppearancePage from "~/settings/my-account/appearance-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("appearance"), + (t) => t("appearance_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/my-account/calendars/page.tsx b/apps/web/app/future/settings/(settings)/my-account/calendars/page.tsx new file mode 100644 index 0000000000..4b5b25885a --- /dev/null +++ b/apps/web/app/future/settings/(settings)/my-account/calendars/page.tsx @@ -0,0 +1,41 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { Button } from "@calcom/ui"; + +import { CalendarListContainer } from "@components/apps/CalendarListContainer"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("calendars"), + (t) => t("calendars_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + const t = await getFixedT(session?.user.locale || "en"); + + const AddCalendarButton = () => { + return ( + <> + + + ); + }; + + return ( + }> + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/my-account/conferencing/page.tsx b/apps/web/app/future/settings/(settings)/my-account/conferencing/page.tsx new file mode 100644 index 0000000000..add5f7dd58 --- /dev/null +++ b/apps/web/app/future/settings/(settings)/my-account/conferencing/page.tsx @@ -0,0 +1,40 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { Button } from "@calcom/ui"; + +import ConferencingView from "~/settings/my-account/conferencing-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("conferencing"), + (t) => t("conferencing_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + const t = await getFixedT(session?.user.locale || "en"); + + const AddConferencingButton = () => { + return ( + + ); + }; + + return ( + } + borderInShellHeader={true}> + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/my-account/general/page.tsx b/apps/web/app/future/settings/(settings)/my-account/general/page.tsx new file mode 100644 index 0000000000..773337ae07 --- /dev/null +++ b/apps/web/app/future/settings/(settings)/my-account/general/page.tsx @@ -0,0 +1,27 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import GeneralQueryView from "~/settings/my-account/general-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("general"), + (t) => t("general_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/my-account/out-of-office/page.tsx b/apps/web/app/future/settings/(settings)/my-account/out-of-office/page.tsx new file mode 100644 index 0000000000..993615b0a6 --- /dev/null +++ b/apps/web/app/future/settings/(settings)/my-account/out-of-office/page.tsx @@ -0,0 +1,31 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import CreateNewOutOfOfficeEntryButton from "@calcom/features/settings/outOfOffice/CreateNewOutOfOfficeEntryButton"; +import { OutOfOfficeEntriesList } from "@calcom/features/settings/outOfOffice/OutOfOfficeEntriesList"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("out_of_office"), + (t) => t("out_of_office_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + }> + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/my-account/profile/page.tsx b/apps/web/app/future/settings/(settings)/my-account/profile/page.tsx new file mode 100644 index 0000000000..3504f51af9 --- /dev/null +++ b/apps/web/app/future/settings/(settings)/my-account/profile/page.tsx @@ -0,0 +1,31 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; +import { APP_NAME } from "@calcom/lib/constants"; + +import ProfileView from "~/settings/my-account/profile-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("profile"), + (t) => t("profile_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/organizations/[id]/about/page.tsx b/apps/web/app/future/settings/(settings)/organizations/[id]/about/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/[id]/about/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/[id]/about/page.tsx diff --git a/apps/web/app/future/settings/organizations/[id]/add-teams/page.tsx b/apps/web/app/future/settings/(settings)/organizations/[id]/add-teams/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/[id]/add-teams/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/[id]/add-teams/page.tsx diff --git a/apps/web/app/future/settings/organizations/[id]/onboard-admins/page.tsx b/apps/web/app/future/settings/(settings)/organizations/[id]/onboard-admins/page.tsx similarity index 89% rename from apps/web/app/future/settings/organizations/[id]/onboard-admins/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/[id]/onboard-admins/page.tsx index 5337e258e9..3bac3fb49d 100644 --- a/apps/web/app/future/settings/organizations/[id]/onboard-admins/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/[id]/onboard-admins/page.tsx @@ -1,16 +1,12 @@ import LegacyPage, { buildWrappedOnboardTeamMembersPage, } from "@pages/settings/organizations/[id]/onboard-members"; -import { type Params } from "app/_types"; +import type { PageProps } from "app/_types"; import { _generateMetadata } from "app/_utils"; import { headers } from "next/headers"; import PageWrapper from "@components/PageWrapperAppDir"; -type PageProps = Readonly<{ - params: Params; -}>; - export const generateMetadata = async () => await _generateMetadata( (t) => t("invite_organization_admins"), diff --git a/apps/web/app/future/settings/(settings)/organizations/admin-api/page.tsx b/apps/web/app/future/settings/(settings)/organizations/admin-api/page.tsx new file mode 100644 index 0000000000..8f5b2ddea0 --- /dev/null +++ b/apps/web/app/future/settings/(settings)/organizations/admin-api/page.tsx @@ -0,0 +1,29 @@ +import { getFixedT, _generateMetadata } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import { AdminAPIView } from "@calcom/features/ee/organizations/pages/settings/admin-api"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => `${t("admin")} ${t("api_reference")}`, + (t) => t("leverage_our_api") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/my-account/appearance/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx similarity index 51% rename from apps/web/app/future/settings/my-account/appearance/layout.tsx rename to apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx index 230bfea4d1..517645e71f 100644 --- a/apps/web/app/future/settings/my-account/appearance/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/appearance/layout.tsx @@ -1,5 +1,5 @@ import { WithLayout } from "app/layoutHOC"; -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/appearance/page.tsx b/apps/web/app/future/settings/(settings)/organizations/appearance/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/appearance/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/appearance/page.tsx diff --git a/apps/web/app/future/settings/my-account/calendars/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx similarity index 51% rename from apps/web/app/future/settings/my-account/calendars/layout.tsx rename to apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx index 230bfea4d1..517645e71f 100644 --- a/apps/web/app/future/settings/my-account/calendars/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/billing/layout.tsx @@ -1,5 +1,5 @@ import { WithLayout } from "app/layoutHOC"; -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/billing/page.tsx b/apps/web/app/future/settings/(settings)/organizations/billing/page.tsx similarity index 80% rename from apps/web/app/future/settings/organizations/billing/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/billing/page.tsx index 7dbae67d41..52080e55f5 100644 --- a/apps/web/app/future/settings/organizations/billing/page.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/billing/page.tsx @@ -1,6 +1,7 @@ -import Page from "@pages/settings/billing/index"; import { _generateMetadata } from "app/_utils"; +import Page from "~/settings/billing/billing-view"; + export const generateMetadata = async () => await _generateMetadata( (t) => t("billing"), diff --git a/apps/web/app/future/settings/my-account/conferencing/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/general/layout.tsx similarity index 51% rename from apps/web/app/future/settings/my-account/conferencing/layout.tsx rename to apps/web/app/future/settings/(settings)/organizations/general/layout.tsx index 230bfea4d1..517645e71f 100644 --- a/apps/web/app/future/settings/my-account/conferencing/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/general/layout.tsx @@ -1,5 +1,5 @@ import { WithLayout } from "app/layoutHOC"; -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/general/page.tsx b/apps/web/app/future/settings/(settings)/organizations/general/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/general/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/general/page.tsx diff --git a/apps/web/app/future/settings/my-account/general/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/members/layout.tsx similarity index 51% rename from apps/web/app/future/settings/my-account/general/layout.tsx rename to apps/web/app/future/settings/(settings)/organizations/members/layout.tsx index 230bfea4d1..517645e71f 100644 --- a/apps/web/app/future/settings/my-account/general/layout.tsx +++ b/apps/web/app/future/settings/(settings)/organizations/members/layout.tsx @@ -1,5 +1,5 @@ import { WithLayout } from "app/layoutHOC"; -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/members/page.tsx b/apps/web/app/future/settings/(settings)/organizations/members/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/members/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/members/page.tsx diff --git a/apps/web/app/future/settings/organizations/new/page.tsx b/apps/web/app/future/settings/(settings)/organizations/new/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/new/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/new/page.tsx diff --git a/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx new file mode 100644 index 0000000000..517645e71f --- /dev/null +++ b/apps/web/app/future/settings/(settings)/organizations/profile/layout.tsx @@ -0,0 +1,5 @@ +import { WithLayout } from "app/layoutHOC"; + +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; + +export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/profile/page.tsx b/apps/web/app/future/settings/(settings)/organizations/profile/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/profile/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/profile/page.tsx diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx new file mode 100644 index 0000000000..517645e71f --- /dev/null +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/layout.tsx @@ -0,0 +1,5 @@ +import { WithLayout } from "app/layoutHOC"; + +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; + +export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/teams/other/[id]/appearance/page.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/teams/other/[id]/appearance/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/appearance/page.tsx diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx new file mode 100644 index 0000000000..517645e71f --- /dev/null +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/layout.tsx @@ -0,0 +1,5 @@ +import { WithLayout } from "app/layoutHOC"; + +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; + +export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/teams/other/[id]/members/page.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/teams/other/[id]/members/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/members/page.tsx diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx new file mode 100644 index 0000000000..517645e71f --- /dev/null +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/layout.tsx @@ -0,0 +1,5 @@ +import { WithLayout } from "app/layoutHOC"; + +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; + +export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/teams/other/[id]/profile/page.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/page.tsx similarity index 100% rename from apps/web/app/future/settings/organizations/teams/other/[id]/profile/page.tsx rename to apps/web/app/future/settings/(settings)/organizations/teams/other/[id]/profile/page.tsx diff --git a/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx b/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx new file mode 100644 index 0000000000..517645e71f --- /dev/null +++ b/apps/web/app/future/settings/(settings)/organizations/teams/other/layout.tsx @@ -0,0 +1,5 @@ +import { WithLayout } from "app/layoutHOC"; + +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; + +export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/teams/other/page.ts b/apps/web/app/future/settings/(settings)/organizations/teams/other/page.ts similarity index 100% rename from apps/web/app/future/settings/organizations/teams/other/page.ts rename to apps/web/app/future/settings/(settings)/organizations/teams/other/page.ts diff --git a/apps/web/app/future/settings/(settings)/security/impersonation/page.tsx b/apps/web/app/future/settings/(settings)/security/impersonation/page.tsx new file mode 100644 index 0000000000..4e20939393 --- /dev/null +++ b/apps/web/app/future/settings/(settings)/security/impersonation/page.tsx @@ -0,0 +1,31 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import ProfileImpersonationViewWrapper from "~/settings/security/impersonation-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("impersonation"), + (t) => t("impersonation_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/security/password/page.tsx b/apps/web/app/future/settings/(settings)/security/password/page.tsx new file mode 100644 index 0000000000..09a6d0e68f --- /dev/null +++ b/apps/web/app/future/settings/(settings)/security/password/page.tsx @@ -0,0 +1,28 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import PasswordViewWrapper from "~/settings/security/password-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("password"), + (t) => t("password_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/security/sso/page.tsx b/apps/web/app/future/settings/(settings)/security/sso/page.tsx new file mode 100644 index 0000000000..b5f311b369 --- /dev/null +++ b/apps/web/app/future/settings/(settings)/security/sso/page.tsx @@ -0,0 +1,30 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SAMLSSO from "@calcom/features/ee/sso/page/user-sso-view"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("sso_configuration"), + (t) => t("sso_configuration_description") + ); + +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/(settings)/security/two-factor-auth/page.tsx b/apps/web/app/future/settings/(settings)/security/two-factor-auth/page.tsx new file mode 100644 index 0000000000..dbfed4e9ab --- /dev/null +++ b/apps/web/app/future/settings/(settings)/security/two-factor-auth/page.tsx @@ -0,0 +1,30 @@ +import { _generateMetadata } from "app/_utils"; +import { getFixedT } from "app/_utils"; +import { getServerSession } from "next-auth"; + +import { AUTH_OPTIONS } from "@calcom/feature-auth/lib/next-auth-options"; +import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader"; + +import TwoFactorAuthView from "~/settings/security/two-factor-auth-view"; + +export const generateMetadata = async () => + await _generateMetadata( + (t) => t("two_factor_auth"), + (t) => t("add_an_extra_layer_of_security") + ); +const Page = async () => { + const session = await getServerSession(AUTH_OPTIONS); + + const t = await getFixedT(session?.user.locale || "en"); + + return ( + + + + ); +}; + +export default Page; diff --git a/apps/web/app/future/settings/teams/[id]/appearance/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/appearance/page.tsx similarity index 100% rename from apps/web/app/future/settings/teams/[id]/appearance/page.tsx rename to apps/web/app/future/settings/(settings)/teams/[id]/appearance/page.tsx diff --git a/apps/web/app/future/settings/teams/[id]/billing/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/billing/page.tsx similarity index 80% rename from apps/web/app/future/settings/teams/[id]/billing/page.tsx rename to apps/web/app/future/settings/(settings)/teams/[id]/billing/page.tsx index 96b7f2f3b3..34e7f004a4 100644 --- a/apps/web/app/future/settings/teams/[id]/billing/page.tsx +++ b/apps/web/app/future/settings/(settings)/teams/[id]/billing/page.tsx @@ -1,6 +1,7 @@ -import Page from "@pages/settings/billing/index"; import { _generateMetadata } from "app/_utils"; +import Page from "~/settings/billing/billing-view"; + export const generateMetadata = async () => await _generateMetadata( (t) => t("billing"), diff --git a/apps/web/app/future/settings/teams/[id]/event-type/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/event-type/page.tsx similarity index 100% rename from apps/web/app/future/settings/teams/[id]/event-type/page.tsx rename to apps/web/app/future/settings/(settings)/teams/[id]/event-type/page.tsx diff --git a/apps/web/app/future/settings/teams/[id]/members/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/members/page.tsx similarity index 100% rename from apps/web/app/future/settings/teams/[id]/members/page.tsx rename to apps/web/app/future/settings/(settings)/teams/[id]/members/page.tsx diff --git a/apps/web/app/future/settings/teams/[id]/onboard-members/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/onboard-members/page.tsx similarity index 100% rename from apps/web/app/future/settings/teams/[id]/onboard-members/page.tsx rename to apps/web/app/future/settings/(settings)/teams/[id]/onboard-members/page.tsx diff --git a/apps/web/app/future/settings/teams/[id]/profile/page.tsx b/apps/web/app/future/settings/(settings)/teams/[id]/profile/page.tsx similarity index 100% rename from apps/web/app/future/settings/teams/[id]/profile/page.tsx rename to apps/web/app/future/settings/(settings)/teams/[id]/profile/page.tsx diff --git a/apps/web/app/future/settings/admin/flags/layout.tsx b/apps/web/app/future/settings/(settings)/teams/layout.tsx similarity index 52% rename from apps/web/app/future/settings/admin/flags/layout.tsx rename to apps/web/app/future/settings/(settings)/teams/layout.tsx index f415bae764..31b58d652c 100644 --- a/apps/web/app/future/settings/admin/flags/layout.tsx +++ b/apps/web/app/future/settings/(settings)/teams/layout.tsx @@ -1,5 +1,5 @@ import { WithLayout } from "app/layoutHOC"; -import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir"; +import { getLayout } from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; export default WithLayout({ getLayout })<"L">; diff --git a/apps/web/app/future/settings/teams/new/page.tsx b/apps/web/app/future/settings/(settings)/teams/new/page.tsx similarity index 100% rename from apps/web/app/future/settings/teams/new/page.tsx rename to apps/web/app/future/settings/(settings)/teams/new/page.tsx diff --git a/apps/web/app/future/settings/teams/page.ts b/apps/web/app/future/settings/(settings)/teams/page.ts similarity index 100% rename from apps/web/app/future/settings/teams/page.ts rename to apps/web/app/future/settings/(settings)/teams/page.ts diff --git a/apps/web/app/future/settings/admin/apps/page.tsx b/apps/web/app/future/settings/admin/apps/page.tsx deleted file mode 100644 index c53f6eb9f9..0000000000 --- a/apps/web/app/future/settings/admin/apps/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/admin/apps/index"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("apps"), - (t) => t("admin_apps_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/admin/flags/page.tsx b/apps/web/app/future/settings/admin/flags/page.tsx deleted file mode 100644 index adf10ecbc3..0000000000 --- a/apps/web/app/future/settings/admin/flags/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/admin/flags"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - () => "Feature Flags", - () => "Here you can toggle your Cal.com instance features." - ); - -export default Page; diff --git a/apps/web/app/future/settings/admin/impersonation/layout.tsx b/apps/web/app/future/settings/admin/impersonation/layout.tsx deleted file mode 100644 index f415bae764..0000000000 --- a/apps/web/app/future/settings/admin/impersonation/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir"; - -export default WithLayout({ getLayout })<"L">; diff --git a/apps/web/app/future/settings/admin/impersonation/page.tsx b/apps/web/app/future/settings/admin/impersonation/page.tsx deleted file mode 100644 index f18a6df858..0000000000 --- a/apps/web/app/future/settings/admin/impersonation/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/admin/impersonation"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("admin"), - (t) => t("impersonation") - ); - -export default Page; diff --git a/apps/web/app/future/settings/admin/oAuth/oAuthView/layout.tsx b/apps/web/app/future/settings/admin/oAuth/oAuthView/layout.tsx deleted file mode 100644 index dc2fb3468f..0000000000 --- a/apps/web/app/future/settings/admin/oAuth/oAuthView/layout.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -export default WithLayout({ getLayout: null })<"L">; diff --git a/apps/web/app/future/settings/admin/oAuth/oAuthView/page.tsx b/apps/web/app/future/settings/admin/oAuth/oAuthView/page.tsx deleted file mode 100644 index 165622e62a..0000000000 --- a/apps/web/app/future/settings/admin/oAuth/oAuthView/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/admin/oAuth/oAuthView"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - () => "OAuth", - () => "Add new OAuth Clients" - ); - -export default Page; diff --git a/apps/web/app/future/settings/admin/oAuth/page.tsx b/apps/web/app/future/settings/admin/oAuth/page.tsx deleted file mode 100644 index 58a8a41dd6..0000000000 --- a/apps/web/app/future/settings/admin/oAuth/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import LegacyPage from "@pages/settings/admin/oAuth/index"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@components/auth/layouts/AdminLayoutAppDir"; - -export const generateMetadata = async () => - await _generateMetadata( - () => "OAuth", - () => "Add new OAuth Clients" - ); - -export default WithLayout({ getLayout, Page: LegacyPage })<"P">; diff --git a/apps/web/app/future/settings/admin/organizations/layout.tsx b/apps/web/app/future/settings/admin/organizations/layout.tsx deleted file mode 100644 index 1359b26601..0000000000 --- a/apps/web/app/future/settings/admin/organizations/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout })<"L">; diff --git a/apps/web/app/future/settings/admin/organizations/page.tsx b/apps/web/app/future/settings/admin/organizations/page.tsx deleted file mode 100644 index 7ec2b7fa6b..0000000000 --- a/apps/web/app/future/settings/admin/organizations/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { _generateMetadata } from "app/_utils"; - -import Page from "@calcom/features/ee/organizations/pages/settings/admin/AdminOrgPage"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("organizations"), - (t) => t("orgs_page_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/admin/users/[id]/edit/page.tsx b/apps/web/app/future/settings/admin/users/[id]/edit/page.tsx deleted file mode 100644 index b9291227c7..0000000000 --- a/apps/web/app/future/settings/admin/users/[id]/edit/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { type Params } from "app/_types"; -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; -import { z } from "zod"; - -import Page from "@calcom/features/ee/users/pages/users-edit-view"; -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -const userIdSchema = z.object({ id: z.coerce.number() }); - -export const generateMetadata = async ({ params }: { params: Params }) => { - const input = userIdSchema.safeParse(params); - if (!input.success) { - return await _generateMetadata( - () => "", - () => "Here you can edit a current user." - ); - } - - const userId = input.data.id; - const { trpc } = await import("@calcom/trpc"); - const [data] = trpc.viewer.users.get.useSuspenseQuery({ userId }); - const { user } = data; - const title = `Editing user: ${user.username}`; - - return await _generateMetadata( - () => title, - () => "Here you can edit a current user." - ); -}; - -export default WithLayout({ getLayout, Page })<"P">; diff --git a/apps/web/app/future/settings/admin/users/add/page.tsx b/apps/web/app/future/settings/admin/users/add/page.tsx deleted file mode 100644 index b9ee07ef27..0000000000 --- a/apps/web/app/future/settings/admin/users/add/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; - -import Page from "@calcom/features/ee/users/pages/users-add-view"; -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export const generateMetadata = async () => - await _generateMetadata( - () => "Add new user", - () => "Here you can add a new user." - ); - -export default WithLayout({ getLayout, Page })<"P">; diff --git a/apps/web/app/future/settings/admin/users/page.tsx b/apps/web/app/future/settings/admin/users/page.tsx deleted file mode 100644 index b78abbdd47..0000000000 --- a/apps/web/app/future/settings/admin/users/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { _generateMetadata } from "app/_utils"; -import { WithLayout } from "app/layoutHOC"; - -import Page from "@calcom/features/ee/users/pages/users-listing-view"; -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export const generateMetadata = async () => - await _generateMetadata( - () => "Users", - () => "A list of all the users in your account including their name, title, email and role." - ); - -export default WithLayout({ getLayout, Page })<"P">; diff --git a/apps/web/app/future/settings/my-account/appearance/page.tsx b/apps/web/app/future/settings/my-account/appearance/page.tsx deleted file mode 100644 index 73def1169e..0000000000 --- a/apps/web/app/future/settings/my-account/appearance/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/my-account/appearance"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("appearance"), - (t) => t("appearance_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/my-account/calendars/page.tsx b/apps/web/app/future/settings/my-account/calendars/page.tsx deleted file mode 100644 index b44eeadcbe..0000000000 --- a/apps/web/app/future/settings/my-account/calendars/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/my-account/calendars"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("calendars"), - (t) => t("calendars_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/my-account/conferencing/page.tsx b/apps/web/app/future/settings/my-account/conferencing/page.tsx deleted file mode 100644 index ce3edb408d..0000000000 --- a/apps/web/app/future/settings/my-account/conferencing/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/my-account/conferencing"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("conferencing"), - (t) => t("conferencing_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/my-account/general/page.tsx b/apps/web/app/future/settings/my-account/general/page.tsx deleted file mode 100644 index a768e8f608..0000000000 --- a/apps/web/app/future/settings/my-account/general/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/my-account/general"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("general"), - (t) => t("general_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/my-account/profile/layout.tsx b/apps/web/app/future/settings/my-account/profile/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/my-account/profile/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/my-account/profile/page.tsx b/apps/web/app/future/settings/my-account/profile/page.tsx deleted file mode 100644 index 2d850a0df6..0000000000 --- a/apps/web/app/future/settings/my-account/profile/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/my-account/profile"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("profile"), - (t) => t("profile_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/organizations/appearance/layout.tsx b/apps/web/app/future/settings/organizations/appearance/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/organizations/appearance/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/billing/layout.tsx b/apps/web/app/future/settings/organizations/billing/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/organizations/billing/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/general/layout.tsx b/apps/web/app/future/settings/organizations/general/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/organizations/general/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/members/layout.tsx b/apps/web/app/future/settings/organizations/members/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/organizations/members/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/profile/layout.tsx b/apps/web/app/future/settings/organizations/profile/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/organizations/profile/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/teams/other/[id]/appearance/layout.tsx b/apps/web/app/future/settings/organizations/teams/other/[id]/appearance/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/organizations/teams/other/[id]/appearance/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/teams/other/[id]/members/layout.tsx b/apps/web/app/future/settings/organizations/teams/other/[id]/members/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/organizations/teams/other/[id]/members/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/teams/other/[id]/profile/layout.tsx b/apps/web/app/future/settings/organizations/teams/other/[id]/profile/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/organizations/teams/other/[id]/profile/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/organizations/teams/other/layout.tsx b/apps/web/app/future/settings/organizations/teams/other/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/organizations/teams/other/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/security/impersonation/layout.tsx b/apps/web/app/future/settings/security/impersonation/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/security/impersonation/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/security/impersonation/page.tsx b/apps/web/app/future/settings/security/impersonation/page.tsx deleted file mode 100644 index 2fa8e4fec2..0000000000 --- a/apps/web/app/future/settings/security/impersonation/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/security/impersonation"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("impersonation"), - (t) => t("impersonation_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/security/password/layout.tsx b/apps/web/app/future/settings/security/password/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/security/password/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/security/password/page.tsx b/apps/web/app/future/settings/security/password/page.tsx deleted file mode 100644 index acf5fe37b2..0000000000 --- a/apps/web/app/future/settings/security/password/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/security/password"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("password"), - (t) => t("password_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/security/sso/layout.tsx b/apps/web/app/future/settings/security/sso/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/security/sso/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/security/sso/page.tsx b/apps/web/app/future/settings/security/sso/page.tsx deleted file mode 100644 index 996ac7c913..0000000000 --- a/apps/web/app/future/settings/security/sso/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { _generateMetadata } from "app/_utils"; - -import Page from "@calcom/features/ee/sso/page/user-sso-view"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("sso_configuration"), - (t) => t("sso_configuration_description") - ); - -export default Page; diff --git a/apps/web/app/future/settings/security/two-factor-auth/layout.tsx b/apps/web/app/future/settings/security/two-factor-auth/layout.tsx deleted file mode 100644 index 230bfea4d1..0000000000 --- a/apps/web/app/future/settings/security/two-factor-auth/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout }); diff --git a/apps/web/app/future/settings/security/two-factor-auth/page.tsx b/apps/web/app/future/settings/security/two-factor-auth/page.tsx deleted file mode 100644 index 55e16afd17..0000000000 --- a/apps/web/app/future/settings/security/two-factor-auth/page.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import Page from "@pages/settings/security/two-factor-auth"; -import { _generateMetadata } from "app/_utils"; - -export const generateMetadata = async () => - await _generateMetadata( - (t) => t("two_factor_auth"), - (t) => t("add_an_extra_layer_of_security") - ); - -export default Page; diff --git a/apps/web/app/future/settings/teams/layout.tsx b/apps/web/app/future/settings/teams/layout.tsx deleted file mode 100644 index 1359b26601..0000000000 --- a/apps/web/app/future/settings/teams/layout.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { WithLayout } from "app/layoutHOC"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayoutAppDir"; - -export default WithLayout({ getLayout })<"L">; diff --git a/apps/web/components/apps/CalendarListContainer.tsx b/apps/web/components/apps/CalendarListContainer.tsx index 953d63f9ee..2e956fcc16 100644 --- a/apps/web/components/apps/CalendarListContainer.tsx +++ b/apps/web/components/apps/CalendarListContainer.tsx @@ -1,4 +1,6 @@ -import { Fragment, useEffect } from "react"; +"use client"; + +import { useEffect } from "react"; import { InstallAppButton } from "@calcom/app-store/components"; import { diff --git a/apps/web/components/auth/layouts/AdminLayout.tsx b/apps/web/components/auth/layouts/AdminLayout.tsx index 5c51ef47ce..6c32d31bc3 100644 --- a/apps/web/components/auth/layouts/AdminLayout.tsx +++ b/apps/web/components/auth/layouts/AdminLayout.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useSession } from "next-auth/react"; import { usePathname, useRouter } from "next/navigation"; import type { ComponentProps } from "react"; diff --git a/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx b/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx index b3d76190c4..e07c877b68 100644 --- a/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx +++ b/apps/web/components/auth/layouts/AdminLayoutAppDir.tsx @@ -5,7 +5,7 @@ import { usePathname, useRouter } from "next/navigation"; import type { ComponentProps } from "react"; import React, { useEffect } from "react"; -import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout"; +import SettingsLayout from "@calcom/features/settings/appDir/SettingsLayoutAppDir"; import type Shell from "@calcom/features/shell/Shell"; import { UserPermissionRole } from "@calcom/prisma/enums"; import { ErrorBoundary } from "@calcom/ui"; diff --git a/apps/web/components/settings/CustomEmailTextField.tsx b/apps/web/components/settings/CustomEmailTextField.tsx index f489d3035a..22417a4f69 100644 --- a/apps/web/components/settings/CustomEmailTextField.tsx +++ b/apps/web/components/settings/CustomEmailTextField.tsx @@ -1,4 +1,3 @@ -import type { FormValues } from "@pages/settings/my-account/profile"; import { useState } from "react"; import type { UseFormReturn } from "react-hook-form"; @@ -15,6 +14,8 @@ import { InputError, } from "@calcom/ui"; +import type { FormValues } from "~/settings/my-account/profile-view"; + type CustomEmailTextFieldProps = { formMethods: UseFormReturn; formMethodFieldName: keyof FormValues; diff --git a/apps/web/components/settings/TravelScheduleModal.tsx b/apps/web/components/settings/TravelScheduleModal.tsx index 2c72d73cfe..ecea714689 100644 --- a/apps/web/components/settings/TravelScheduleModal.tsx +++ b/apps/web/components/settings/TravelScheduleModal.tsx @@ -1,4 +1,3 @@ -import type { FormValues } from "@pages/settings/my-account/general"; import { useState } from "react"; import type { UseFormSetValue } from "react-hook-form"; @@ -18,6 +17,8 @@ import { DatePicker, } from "@calcom/ui"; +import type { FormValues } from "~/settings/my-account/general-view"; + interface TravelScheduleModalProps { open: boolean; onOpenChange: () => void; diff --git a/apps/web/lib/settings/admin/orgMigrations/moveTeamToOrg/getServerSideProps.tsx b/apps/web/lib/settings/admin/orgMigrations/moveTeamToOrg/getServerSideProps.tsx new file mode 100644 index 0000000000..cb4ac548b5 --- /dev/null +++ b/apps/web/lib/settings/admin/orgMigrations/moveTeamToOrg/getServerSideProps.tsx @@ -0,0 +1,36 @@ +import type { GetServerSidePropsContext } from "next"; +import { getSession } from "next-auth/react"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; + +import { UserPermissionRole } from "@calcom/prisma/enums"; + +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const session = await getSession(ctx); + if (!session || !session.user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + const isAdmin = session.user.role === UserPermissionRole.ADMIN; + if (!isAdmin) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + return { + props: { + error: null, + migrated: null, + userId: session.user.id, + ...(await serverSideTranslations(ctx.locale || "en", ["common"])), + username: session.user.username, + }, + }; +} diff --git a/apps/web/lib/settings/admin/orgMigrations/moveUserToOrg/getServerSideProps.tsx b/apps/web/lib/settings/admin/orgMigrations/moveUserToOrg/getServerSideProps.tsx new file mode 100644 index 0000000000..be728f4d8c --- /dev/null +++ b/apps/web/lib/settings/admin/orgMigrations/moveUserToOrg/getServerSideProps.tsx @@ -0,0 +1,32 @@ +import type { GetServerSidePropsContext } from "next"; +import { getSession } from "next-auth/react"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; + +import { UserPermissionRole } from "@calcom/prisma/enums"; + +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const session = await getSession(ctx); + if (!session || !session.user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + const isAdmin = session.user.role === UserPermissionRole.ADMIN; + if (!isAdmin) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + return { + props: { + ...(await serverSideTranslations(ctx.locale || "en", ["common"])), + }, + }; +} diff --git a/apps/web/lib/settings/admin/orgMigrations/removeTeamFromOrg/getServerSideProps.tsx b/apps/web/lib/settings/admin/orgMigrations/removeTeamFromOrg/getServerSideProps.tsx new file mode 100644 index 0000000000..be728f4d8c --- /dev/null +++ b/apps/web/lib/settings/admin/orgMigrations/removeTeamFromOrg/getServerSideProps.tsx @@ -0,0 +1,32 @@ +import type { GetServerSidePropsContext } from "next"; +import { getSession } from "next-auth/react"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; + +import { UserPermissionRole } from "@calcom/prisma/enums"; + +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const session = await getSession(ctx); + if (!session || !session.user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + const isAdmin = session.user.role === UserPermissionRole.ADMIN; + if (!isAdmin) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + return { + props: { + ...(await serverSideTranslations(ctx.locale || "en", ["common"])), + }, + }; +} diff --git a/apps/web/lib/settings/admin/orgMigrations/removeUserFromOrg/getServerSideProps.tsx b/apps/web/lib/settings/admin/orgMigrations/removeUserFromOrg/getServerSideProps.tsx new file mode 100644 index 0000000000..be728f4d8c --- /dev/null +++ b/apps/web/lib/settings/admin/orgMigrations/removeUserFromOrg/getServerSideProps.tsx @@ -0,0 +1,32 @@ +import type { GetServerSidePropsContext } from "next"; +import { getSession } from "next-auth/react"; +import { serverSideTranslations } from "next-i18next/serverSideTranslations"; + +import { UserPermissionRole } from "@calcom/prisma/enums"; + +export async function getServerSideProps(ctx: GetServerSidePropsContext) { + const session = await getSession(ctx); + if (!session || !session.user) { + return { + redirect: { + destination: "/login", + permanent: false, + }, + }; + } + + const isAdmin = session.user.role === UserPermissionRole.ADMIN; + if (!isAdmin) { + return { + redirect: { + destination: "/", + permanent: false, + }, + }; + } + return { + props: { + ...(await serverSideTranslations(ctx.locale || "en", ["common"])), + }, + }; +} diff --git a/apps/web/pages/settings/admin/lockedSMS/UsersTable.tsx b/apps/web/modules/settings/admin/components/UsersTable.tsx similarity index 100% rename from apps/web/pages/settings/admin/lockedSMS/UsersTable.tsx rename to apps/web/modules/settings/admin/components/UsersTable.tsx diff --git a/apps/web/modules/settings/admin/impersonation-view.tsx b/apps/web/modules/settings/admin/impersonation-view.tsx new file mode 100644 index 0000000000..9331377a7d --- /dev/null +++ b/apps/web/modules/settings/admin/impersonation-view.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { signIn } from "next-auth/react"; +import { useSearchParams } from "next/navigation"; +import { useRef, useEffect } from "react"; + +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, TextField } from "@calcom/ui"; + +const ImpersonationView = () => { + const { t } = useLocale(); + const usernameRef = useRef(null); + const searchParams = useSearchParams(); + + const username = searchParams?.get("username")?.toLowerCase(); + + useEffect(() => { + if (username) { + const enteredUsername = username.toLowerCase(); + signIn("impersonation-auth", { + username: enteredUsername, + callbackUrl: `${WEBAPP_URL}/event-types`, + }); + } + }, [username]); + + return ( +
{ + e.preventDefault(); + const enteredUsername = usernameRef.current?.value.toLowerCase(); + signIn("impersonation-auth", { + username: enteredUsername, + callbackUrl: `${WEBAPP_URL}/event-types`, + }); + }}> +
+ {process.env.NEXT_PUBLIC_WEBSITE_URL}/} + ref={usernameRef} + hint={t("impersonate_user_tip")} + defaultValue={undefined} + data-testid="admin-impersonation-input" + /> + +
+
+ ); +}; + +export default ImpersonationView; diff --git a/apps/web/pages/settings/admin/lockedSMS/lockedSMSView.tsx b/apps/web/modules/settings/admin/locked-sms-view.tsx similarity index 91% rename from apps/web/pages/settings/admin/lockedSMS/lockedSMSView.tsx rename to apps/web/modules/settings/admin/locked-sms-view.tsx index 079586abd6..eb6364942b 100644 --- a/apps/web/pages/settings/admin/lockedSMS/lockedSMSView.tsx +++ b/apps/web/modules/settings/admin/locked-sms-view.tsx @@ -1,10 +1,11 @@ "use client"; -import UsersTable from "@pages/settings/admin/lockedSMS/UsersTable"; import { useState } from "react"; import { trpc } from "@calcom/trpc"; -import { Button, Meta, TextField, showToast } from "@calcom/ui"; +import { Button, TextField, showToast } from "@calcom/ui"; + +import UsersTable from "./components/UsersTable"; export default function LockedSMSView() { const [username, setUsername] = useState(""); @@ -35,7 +36,6 @@ export default function LockedSMSView() { return (
-
- {!clientId ? (
{ + return ( + <> +
+
+

{title}

+

{description}

+
+
{children}
+
+ + ); +}; + +const BillingView = () => { + const pathname = usePathname(); + const { t } = useLocale(); + const { open } = useIntercom(); + const returnTo = pathname; + const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`; + + const onContactSupportClick = async () => { + await open(); + }; + + return ( + <> +
+ + + + +
+ + + + +
+ + ); +}; + +export default BillingView; diff --git a/apps/web/modules/settings/developer/api-keys-view.tsx b/apps/web/modules/settings/developer/api-keys-view.tsx new file mode 100644 index 0000000000..34bba40b23 --- /dev/null +++ b/apps/web/modules/settings/developer/api-keys-view.tsx @@ -0,0 +1,108 @@ +"use client"; + +import { useState } from "react"; + +import type { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem"; +import LicenseRequired from "@calcom/ee/common/components/LicenseRequired"; +import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/ApiKeyDialogForm"; +import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListItem"; +import { APP_NAME } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { + Button, + Dialog, + DialogContent, + EmptyScreen, + Meta, + SkeletonContainer, + SkeletonText, +} from "@calcom/ui"; + +const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { + return ( + + +
+ + +
+
+ ); +}; + +const ApiKeysView = () => { + const { t } = useLocale(); + + const { data, isPending } = trpc.viewer.apiKeys.list.useQuery(); + + const [apiKeyModal, setApiKeyModal] = useState(false); + const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | undefined>( + undefined + ); + + const NewApiKeyButton = () => { + return ( + + ); + }; + + if (isPending || !data) { + return ( + + ); + } + + return ( + <> + +
+ {data?.length ? ( + <> +
+ {data.map((apiKey, index) => ( + { + setApiKeyToEdit(apiKey); + setApiKeyModal(true); + }} + /> + ))} +
+ + ) : ( + } + /> + )} +
+
+ + + + setApiKeyModal(false)} defaultValues={apiKeyToEdit} /> + + + + ); +}; + +export default ApiKeysView; diff --git a/apps/web/modules/settings/developer/components/CreateApiKeyButton.tsx b/apps/web/modules/settings/developer/components/CreateApiKeyButton.tsx new file mode 100644 index 0000000000..18a0171d07 --- /dev/null +++ b/apps/web/modules/settings/developer/components/CreateApiKeyButton.tsx @@ -0,0 +1,38 @@ +"use client"; + +import { useState } from "react"; + +import type { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem"; +import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/ApiKeyDialogForm"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button, Dialog, DialogContent } from "@calcom/ui"; + +const NewApiKeyButton = () => { + const { t } = useLocale(); + const [apiKeyModal, setApiKeyModal] = useState(false); + const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | undefined>( + undefined + ); + + return ( + <> + + + + + setApiKeyModal(false)} defaultValues={apiKeyToEdit} /> + + + + ); +}; + +export default NewApiKeyButton; diff --git a/apps/web/modules/settings/my-account/appearance-view.tsx b/apps/web/modules/settings/my-account/appearance-view.tsx new file mode 100644 index 0000000000..fc904d000b --- /dev/null +++ b/apps/web/modules/settings/my-account/appearance-view.tsx @@ -0,0 +1,440 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; +import type { z } from "zod"; + +import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector"; +import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; +import ThemeLabel from "@calcom/features/settings/ThemeLabel"; +import { APP_NAME } from "@calcom/lib/constants"; +import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants"; +import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours"; +import useGetBrandingColours from "@calcom/lib/getBrandColours"; +import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import useTheme from "@calcom/lib/hooks/useTheme"; +import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts"; +import type { userMetadata } from "@calcom/prisma/zod-utils"; +import { trpc } from "@calcom/trpc/react"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { + Alert, + Button, + ColorPicker, + Form, + showToast, + SkeletonButton, + SkeletonContainer, + SkeletonText, + SettingsToggle, + UpgradeTeamsBadge, + useCalcomTheme, +} from "@calcom/ui"; + +const SkeletonLoader = () => { + return ( + +
+ +
+
+
+
+
+
+
+
+ + +
+ + +
+
+ + + +
+ + ); +}; + +const useBrandColors = ( + currentTheme: string | null, + { + brandColor, + darkBrandColor, + }: { + brandColor?: string | null; + darkBrandColor?: string | null; + } +): void => { + const brandTheme = useGetBrandingColours({ + lightVal: brandColor, + darkVal: darkBrandColor, + }); + const selectedTheme = currentTheme ? brandTheme[currentTheme as "light" | "dark"] : {}; + useCalcomTheme({ + root: selectedTheme, + }); +}; + +const AppearanceView = ({ + user, + hasPaidPlan, +}: { + user: RouterOutputs["viewer"]["me"]; + hasPaidPlan: boolean; +}) => { + const { t } = useLocale(); + const utils = trpc.useUtils(); + const session = useSession(); + const isApartOfOrganization = session.data?.user.org?.id; + const [darkModeError, setDarkModeError] = useState(false); + const [lightModeError, setLightModeError] = useState(false); + const [isCustomBrandColorChecked, setIsCustomBranColorChecked] = useState( + user?.brandColor !== DEFAULT_LIGHT_BRAND_COLOR || user?.darkBrandColor !== DEFAULT_DARK_BRAND_COLOR + ); + const [hideBrandingValue, setHideBrandingValue] = useState(user?.hideBranding ?? false); + useTheme(user?.appTheme); + useBrandColors(user?.appTheme ?? null, { + brandColor: user?.brandColor, + darkBrandColor: user?.darkBrandColor, + }); + + const userAppThemeFormMethods = useForm({ + defaultValues: { + appTheme: user.appTheme, + }, + }); + + const { + formState: { isSubmitting: isUserAppThemeSubmitting, isDirty: isUserAppThemeDirty }, + reset: resetUserAppThemeReset, + } = userAppThemeFormMethods; + + const userThemeFormMethods = useForm({ + defaultValues: { + theme: user.theme, + }, + }); + + const { + formState: { isSubmitting: isUserThemeSubmitting, isDirty: isUserThemeDirty }, + reset: resetUserThemeReset, + } = userThemeFormMethods; + + const bookerLayoutFormMethods = useForm({ + defaultValues: { + metadata: user.metadata as z.infer, + }, + }); + + const { + formState: { isSubmitting: isBookerLayoutFormSubmitting, isDirty: isBookerLayoutFormDirty }, + reset: resetBookerLayoutThemeReset, + } = bookerLayoutFormMethods; + + const DEFAULT_BRAND_COLOURS = { + light: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR, + dark: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR, + }; + + const brandColorsFormMethods = useForm({ + defaultValues: { + brandColor: DEFAULT_BRAND_COLOURS.light, + darkBrandColor: DEFAULT_BRAND_COLOURS.dark, + }, + }); + + const { + formState: { isSubmitting: isBrandColorsFormSubmitting, isDirty: isBrandColorsFormDirty }, + reset: resetBrandColorsThemeReset, + } = brandColorsFormMethods; + + const selectedTheme = userThemeFormMethods.watch("theme"); + const selectedThemeIsDark = + selectedTheme === "dark" || + (selectedTheme === "" && + typeof document !== "undefined" && + document.documentElement.classList.contains("dark")); + + const mutation = trpc.viewer.updateProfile.useMutation({ + onSuccess: async (data) => { + await utils.viewer.me.invalidate(); + showToast(t("settings_updated_successfully"), "success"); + resetBrandColorsThemeReset({ brandColor: data.brandColor, darkBrandColor: data.darkBrandColor }); + resetBookerLayoutThemeReset({ metadata: data.metadata }); + resetUserThemeReset({ theme: data.theme }); + resetUserAppThemeReset({ appTheme: data.appTheme }); + }, + onError: (error) => { + if (error.message) { + showToast(error.message, "error"); + } else { + showToast(t("error_updating_settings"), "error"); + } + }, + onSettled: async () => { + await utils.viewer.me.invalidate(); + }, + }); + + return ( +
+
+
+

{t("app_theme")}

+

{t("app_theme_applies_note")}

+
+
+ { + mutation.mutate({ + appTheme: values.appTheme === "" ? null : values.appTheme, + }); + }}> +
+ + + +
+ + + + + + {isApartOfOrganization ? null : ( + <> +
+
+

{t("theme")}

+

{t("theme_applies_note")}

+
+
+
{ + mutation.mutate({ + // Radio values don't support null as values, therefore we convert an empty string + // back to null here. + theme: values.theme === "" ? null : values.theme, + }); + }}> +
+ + + +
+ + + +
+ +
{ + const layoutError = validateBookerLayouts(values?.metadata?.defaultBookerLayouts || null); + if (layoutError) { + showToast(t(layoutError), "error"); + return; + } else { + mutation.mutate(values); + } + }}> + + + +
{ + mutation.mutate(values); + }}> +
+ { + setIsCustomBranColorChecked(checked); + if (!checked) { + mutation.mutate({ + brandColor: DEFAULT_LIGHT_BRAND_COLOR, + darkBrandColor: DEFAULT_DARK_BRAND_COLOR, + }); + } + }} + childrenClassName="lg:ml-0"> +
+ ( +
+

+ {t("light_brand_color")} +

+ { + if (checkWCAGContrastColor("#ffffff", value)) { + setLightModeError(false); + } else { + setLightModeError(true); + } + brandColorsFormMethods.setValue("brandColor", value, { shouldDirty: true }); + }} + /> + {lightModeError ? ( +
+ +
+ ) : null} +
+ )} + /> + + ( +
+

{t("dark_brand_color")}

+ { + if (checkWCAGContrastColor("#101010", value)) { + setDarkModeError(false); + } else { + setDarkModeError(true); + } + brandColorsFormMethods.setValue("darkBrandColor", value, { shouldDirty: true }); + }} + /> + {darkModeError ? ( +
+ +
+ ) : null} +
+ )} + /> +
+ + + +
+
+
+ + {/* TODO future PR to preview brandColors */} + {/* */} + + } + onCheckedChange={(checked) => { + setHideBrandingValue(checked); + mutation.mutate({ hideBranding: checked }); + }} + switchContainerClassName="mt-6" + /> + + )} +
+ ); +}; + +const AppearancePage = () => { + const { data: user, isPending } = trpc.viewer.me.useQuery(); + const { isPending: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan(); + + if (isPending || isTeamPlanStatusLoading || !user) return ; + + return ; +}; + +export default AppearancePage; diff --git a/apps/web/modules/settings/my-account/components/AddCalendarButton.tsx b/apps/web/modules/settings/my-account/components/AddCalendarButton.tsx new file mode 100644 index 0000000000..5634683ef4 --- /dev/null +++ b/apps/web/modules/settings/my-account/components/AddCalendarButton.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Button } from "@calcom/ui"; + +const AddCalendarButton = () => { + const { t } = useLocale(); + + return ( + <> + + + ); +}; + +export default AddCalendarButton; diff --git a/apps/web/modules/settings/my-account/conferencing-view.tsx b/apps/web/modules/settings/my-account/conferencing-view.tsx new file mode 100644 index 0000000000..8a765b95ef --- /dev/null +++ b/apps/web/modules/settings/my-account/conferencing-view.tsx @@ -0,0 +1,101 @@ +"use client"; + +import { useReducer } from "react"; + +import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { Button, EmptyScreen, SkeletonContainer, SkeletonText } from "@calcom/ui"; + +import { QueryCell } from "@lib/QueryCell"; + +import { AppList } from "@components/apps/AppList"; + +const SkeletonLoader = () => { + return ( + +
+ + +
+
+ ); +}; + +type ModalState = { + isOpen: boolean; + credentialId: null | number; +}; + +const ConferencingView = () => { + const { t } = useLocale(); + + const [modal, updateModal] = useReducer( + (data: ModalState, partialData: Partial) => ({ ...data, ...partialData }), + { + isOpen: false, + credentialId: null, + } + ); + + const query = trpc.viewer.integrations.useQuery({ + variant: "conferencing", + onlyInstalled: true, + }); + + const handleModelClose = () => { + updateModal({ isOpen: false, credentialId: null }); + }; + + const handleDisconnect = (credentialId: number) => { + updateModal({ isOpen: true, credentialId }); + }; + + return ( + <> +
+ } + success={({ data }) => { + console.log(data); + if (!data.items.length) { + return ( + + {t("connect_conference_apps")} + + } + /> + ); + } + return ( + + ); + }} + /> +
+ + + ); +}; + +export default ConferencingView; diff --git a/apps/web/modules/settings/my-account/general-view.tsx b/apps/web/modules/settings/my-account/general-view.tsx new file mode 100644 index 0000000000..da82bbecc8 --- /dev/null +++ b/apps/web/modules/settings/my-account/general-view.tsx @@ -0,0 +1,378 @@ +"use client"; + +import { useSession } from "next-auth/react"; +import { useState } from "react"; +import { Controller, useForm } from "react-hook-form"; + +import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; +import { classNames } from "@calcom/lib"; +import { formatLocalizedDateTime } from "@calcom/lib/date-fns"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { localeOptions } from "@calcom/lib/i18n"; +import { nameOfDay } from "@calcom/lib/weekday"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { trpc } from "@calcom/trpc/react"; +import { + Button, + Form, + Label, + Select, + showToast, + SkeletonButton, + SkeletonContainer, + SkeletonText, + TimezoneSelect, + SettingsToggle, +} from "@calcom/ui"; + +import TravelScheduleModal from "@components/settings/TravelScheduleModal"; + +export type FormValues = { + locale: { + value: string; + label: string; + }; + timeZone: string; + timeFormat: { + value: number; + label: string | number; + }; + weekStart: { + value: string; + label: string; + }; + travelSchedules: { + id?: number; + startDate: Date; + endDate?: Date; + timeZone: string; + }[]; +}; + +const SkeletonLoader = () => { + return ( + +
+ + + + + + +
+
+ ); +}; + +interface GeneralViewProps { + localeProp: string; + user: RouterOutputs["viewer"]["me"]; + travelSchedules: RouterOutputs["viewer"]["getTravelSchedules"]; +} + +const GeneralQueryView = () => { + const { t } = useLocale(); + + const { data: user, isPending } = trpc.viewer.me.useQuery(); + + const { data: travelSchedules, isPending: isPendingTravelSchedules } = + trpc.viewer.getTravelSchedules.useQuery(); + + if (isPending || isPendingTravelSchedules) return ; + if (!user) { + throw new Error(t("something_went_wrong")); + } + return ; +}; + +const GeneralView = ({ localeProp, user, travelSchedules }: GeneralViewProps) => { + const utils = trpc.useContext(); + const { + t, + i18n: { language }, + } = useLocale(); + const { update } = useSession(); + const [isUpdateBtnLoading, setIsUpdateBtnLoading] = useState(false); + const [isTZScheduleOpen, setIsTZScheduleOpen] = useState(false); + + const mutation = trpc.viewer.updateProfile.useMutation({ + onSuccess: async (res) => { + await utils.viewer.me.invalidate(); + reset(getValues()); + showToast(t("settings_updated_successfully"), "success"); + await update(res); + + if (res.locale) { + window.calNewLocale = res.locale; + } + }, + onError: () => { + showToast(t("error_updating_settings"), "error"); + }, + onSettled: async () => { + await utils.viewer.me.invalidate(); + setIsUpdateBtnLoading(false); + }, + }); + + const timeFormatOptions = [ + { value: 12, label: t("12_hour") }, + { value: 24, label: t("24_hour") }, + ]; + + const weekStartOptions = [ + { value: "Sunday", label: nameOfDay(localeProp, 0) }, + { value: "Monday", label: nameOfDay(localeProp, 1) }, + { value: "Tuesday", label: nameOfDay(localeProp, 2) }, + { value: "Wednesday", label: nameOfDay(localeProp, 3) }, + { value: "Thursday", label: nameOfDay(localeProp, 4) }, + { value: "Friday", label: nameOfDay(localeProp, 5) }, + { value: "Saturday", label: nameOfDay(localeProp, 6) }, + ]; + + const formMethods = useForm({ + defaultValues: { + locale: { + value: localeProp || "", + label: localeOptions.find((option) => option.value === localeProp)?.label || "", + }, + timeZone: user.timeZone || "", + timeFormat: { + value: user.timeFormat || 12, + label: timeFormatOptions.find((option) => option.value === user.timeFormat)?.label || 12, + }, + weekStart: { + value: user.weekStart, + label: nameOfDay(localeProp, user.weekStart === "Sunday" ? 0 : 1), + }, + travelSchedules: + travelSchedules.map((schedule) => { + return { + id: schedule.id, + startDate: schedule.startDate, + endDate: schedule.endDate ?? undefined, + timeZone: schedule.timeZone, + }; + }) || [], + }, + }); + const { + formState: { isDirty, isSubmitting }, + reset, + getValues, + } = formMethods; + const isDisabled = isSubmitting || !isDirty; + + const [isAllowDynamicBookingChecked, setIsAllowDynamicBookingChecked] = useState( + !!user.allowDynamicBooking + ); + const [isAllowSEOIndexingChecked, setIsAllowSEOIndexingChecked] = useState(!!user.allowSEOIndexing); + const [isReceiveMonthlyDigestEmailChecked, setIsReceiveMonthlyDigestEmailChecked] = useState( + !!user.receiveMonthlyDigestEmail + ); + + const watchedTzSchedules = formMethods.watch("travelSchedules"); + + return ( +
+
{ + setIsUpdateBtnLoading(true); + mutation.mutate({ + ...values, + locale: values.locale.value, + timeFormat: values.timeFormat.value, + weekStart: values.weekStart.value, + }); + }}> +
+ ( + <> + + + className="capitalize" + options={localeOptions} + value={value} + onChange={onChange} + /> + + )} + /> + ( + <> + + { + if (event) formMethods.setValue("timeZone", event.value, { shouldDirty: true }); + }} + /> + + )} + /> + {!watchedTzSchedules.length ? ( + + ) : ( +
+ +
+ {watchedTzSchedules.map((schedule, index) => { + return ( +
+
+
{`${formatLocalizedDateTime( + schedule.startDate, + { day: "numeric", month: "long" }, + language + )} ${ + schedule.endDate + ? `- ${formatLocalizedDateTime( + schedule.endDate, + { day: "numeric", month: "long" }, + language + )}` + : `` + }`}
+
{schedule.timeZone.replace(/_/g, " ")}
+
+
+ ); + })} +
+ +
+ )} + + ( + <> + + { + if (event) formMethods.setValue("weekStart", { ...event }, { shouldDirty: true }); + }} + /> + + )} + /> +
+ + + + +
+ + { + setIsAllowDynamicBookingChecked(checked); + mutation.mutate({ allowDynamicBooking: checked }); + }} + switchContainerClassName="mt-6" + /> + + { + setIsAllowSEOIndexingChecked(checked); + mutation.mutate({ allowSEOIndexing: checked }); + }} + switchContainerClassName="mt-6" + /> + + { + setIsReceiveMonthlyDigestEmailChecked(checked); + mutation.mutate({ receiveMonthlyDigestEmail: checked }); + }} + switchContainerClassName="mt-6" + /> + setIsTZScheduleOpen(false)} + setValue={formMethods.setValue} + existingSchedules={formMethods.getValues("travelSchedules") ?? []} + /> +
+ ); +}; + +export default GeneralQueryView; diff --git a/apps/web/modules/settings/my-account/profile-view.tsx b/apps/web/modules/settings/my-account/profile-view.tsx new file mode 100644 index 0000000000..c737c3cd26 --- /dev/null +++ b/apps/web/modules/settings/my-account/profile-view.tsx @@ -0,0 +1,730 @@ +"use client"; + +import { zodResolver } from "@hookform/resolvers/zod"; +// eslint-disable-next-line no-restricted-imports +import { get, pick } from "lodash"; +import { signOut, useSession } from "next-auth/react"; +import type { BaseSyntheticEvent } from "react"; +import React, { useRef, useState } from "react"; +import { Controller, useFieldArray, useForm } from "react-hook-form"; +import { z } from "zod"; + +import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode"; +import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; +import { APP_NAME, FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants"; +import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { md } from "@calcom/lib/markdownIt"; +import turndown from "@calcom/lib/turndownService"; +import { IdentityProvider } from "@calcom/prisma/enums"; +import type { TRPCClientErrorLike } from "@calcom/trpc/client"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { trpc } from "@calcom/trpc/react"; +import type { AppRouter } from "@calcom/trpc/server/routers/_app"; +import { + Alert, + Button, + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogTrigger, + Editor, + Form, + ImageUploader, + Label, + PasswordField, + showToast, + SkeletonAvatar, + SkeletonButton, + SkeletonContainer, + SkeletonText, + TextField, + UserAvatar, +} from "@calcom/ui"; + +import TwoFactor from "@components/auth/TwoFactor"; +import CustomEmailTextField from "@components/settings/CustomEmailTextField"; +import SecondaryEmailConfirmModal from "@components/settings/SecondaryEmailConfirmModal"; +import SecondaryEmailModal from "@components/settings/SecondaryEmailModal"; +import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability"; + +const SkeletonLoader = () => { + return ( + +
+
+ + +
+ + + + + +
+
+ ); +}; + +interface DeleteAccountValues { + totpCode: string; +} + +type Email = { + id: number; + email: string; + emailVerified: string | null; + emailPrimary: boolean; +}; + +export type FormValues = { + username: string; + avatarUrl: string | null; + name: string; + email: string; + bio: string; + secondaryEmails: Email[]; +}; + +const ProfileView = () => { + const { t } = useLocale(); + const utils = trpc.useUtils(); + const { update } = useSession(); + const { data: user, isPending } = trpc.viewer.me.useQuery({ includePasswordAdded: true }); + + const updateProfileMutation = trpc.viewer.updateProfile.useMutation({ + onSuccess: async (res) => { + await update(res); + utils.viewer.me.invalidate(); + utils.viewer.shouldVerifyEmail.invalidate(); + + if (res.hasEmailBeenChanged && res.sendEmailVerification) { + showToast(t("change_of_email_toast", { email: tempFormValues?.email }), "success"); + } else { + showToast(t("settings_updated_successfully"), "success"); + } + + setTempFormValues(null); + }, + onError: (e) => { + switch (e.message) { + // TODO: Add error codes. + case "email_already_used": + { + showToast(t(e.message), "error"); + } + return; + default: + showToast(t("error_updating_settings"), "error"); + } + }, + }); + const unlinkConnectedAccountMutation = trpc.viewer.unlinkConnectedAccount.useMutation({ + onSuccess: async (res) => { + showToast(t(res.message), "success"); + utils.viewer.me.invalidate(); + }, + onError: (e) => { + showToast(t(e.message), "error"); + }, + }); + + const addSecondaryEmailMutation = trpc.viewer.addSecondaryEmail.useMutation({ + onSuccess: (res) => { + setShowSecondaryEmailModalOpen(false); + setNewlyAddedSecondaryEmail(res?.data?.email); + utils.viewer.me.invalidate(); + }, + onError: (error) => { + setSecondaryEmailAddErrorMessage(error?.message || ""); + }, + }); + + const resendVerifyEmailMutation = trpc.viewer.auth.resendVerifyEmail.useMutation(); + + const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false); + const [tempFormValues, setTempFormValues] = useState(null); + const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState(""); + const [showCreateAccountPasswordDialog, setShowCreateAccountPasswordDialog] = useState(false); + const [showAccountDisconnectWarning, setShowAccountDisconnectWarning] = useState(false); + const [showSecondaryEmailModalOpen, setShowSecondaryEmailModalOpen] = useState(false); + const [secondaryEmailAddErrorMessage, setSecondaryEmailAddErrorMessage] = useState(""); + const [newlyAddedSecondaryEmail, setNewlyAddedSecondaryEmail] = useState(undefined); + + const [deleteAccountOpen, setDeleteAccountOpen] = useState(false); + const [hasDeleteErrors, setHasDeleteErrors] = useState(false); + const [deleteErrorMessage, setDeleteErrorMessage] = useState(""); + const form = useForm(); + + const onDeleteMeSuccessMutation = async () => { + await utils.viewer.me.invalidate(); + showToast(t("Your account was deleted"), "success"); + + setHasDeleteErrors(false); // dismiss any open errors + if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") { + signOut({ callbackUrl: "/auth/logout?survey=true" }); + } else { + signOut({ callbackUrl: "/auth/logout" }); + } + }; + + const confirmPasswordMutation = trpc.viewer.auth.verifyPassword.useMutation({ + onSuccess() { + if (tempFormValues) updateProfileMutation.mutate(tempFormValues); + setConfirmPasswordOpen(false); + }, + onError() { + setConfirmPasswordDeleteErrorMessage(t("incorrect_password")); + }, + }); + + const onDeleteMeErrorMutation = (error: TRPCClientErrorLike) => { + setHasDeleteErrors(true); + setDeleteErrorMessage(errorMessages[error.message]); + }; + const deleteMeMutation = trpc.viewer.deleteMe.useMutation({ + onSuccess: onDeleteMeSuccessMutation, + onError: onDeleteMeErrorMutation, + async onSettled() { + await utils.viewer.me.invalidate(); + }, + }); + const deleteMeWithoutPasswordMutation = trpc.viewer.deleteMeWithoutPassword.useMutation({ + onSuccess: onDeleteMeSuccessMutation, + onError: onDeleteMeErrorMutation, + async onSettled() { + await utils.viewer.me.invalidate(); + }, + }); + + const isCALIdentityProvider = user?.identityProvider === IdentityProvider.CAL; + + const onConfirmPassword = (e: Event | React.MouseEvent) => { + e.preventDefault(); + + const password = passwordRef.current.value; + confirmPasswordMutation.mutate({ passwordInput: password }); + }; + + const onConfirmButton = (e: Event | React.MouseEvent) => { + e.preventDefault(); + if (isCALIdentityProvider) { + const totpCode = form.getValues("totpCode"); + const password = passwordRef.current.value; + deleteMeMutation.mutate({ password, totpCode }); + } else { + deleteMeWithoutPasswordMutation.mutate(); + } + }; + + const onConfirm = ({ totpCode }: DeleteAccountValues, e: BaseSyntheticEvent | undefined) => { + e?.preventDefault(); + if (isCALIdentityProvider) { + const password = passwordRef.current.value; + deleteMeMutation.mutate({ password, totpCode }); + } else { + deleteMeWithoutPasswordMutation.mutate(); + } + }; + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const passwordRef = useRef(null!); + + const errorMessages: { [key: string]: string } = { + [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"), + [ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`, + [ErrorCode.UserNotFound]: t("no_account_exists"), + [ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`, + [ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`, + [ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"), + }; + + if (isPending || !user) { + return ; + } + + const userEmail = user.email || ""; + const defaultValues = { + username: user.username || "", + avatarUrl: user.avatarUrl, + name: user.name || "", + email: userEmail, + bio: user.bio || "", + // We add the primary email as the first item in the list + secondaryEmails: [ + { + id: 0, + email: userEmail, + emailVerified: user.emailVerified?.toString() || null, + emailPrimary: true, + }, + ...(user.secondaryEmails || []).map((secondaryEmail) => ({ + ...secondaryEmail, + emailVerified: secondaryEmail.emailVerified?.toString() || null, + emailPrimary: false, + })), + ], + }; + + return ( + <> + { + if (values.email !== user.email && isCALIdentityProvider) { + setTempFormValues(values); + setConfirmPasswordOpen(true); + } else { + updateProfileMutation.mutate(values); + } + }} + handleAddSecondaryEmail={() => setShowSecondaryEmailModalOpen(true)} + handleResendVerifyEmail={(email) => { + resendVerifyEmailMutation.mutate({ email }); + showToast(t("email_sent"), "success"); + }} + handleAccountDisconnect={(values) => { + if (isCALIdentityProvider) return; + if (user?.passwordAdded) { + setTempFormValues(values); + setShowAccountDisconnectWarning(true); + return; + } + setShowCreateAccountPasswordDialog(true); + }} + extraField={ +
+ { + showToast(t("settings_updated_successfully"), "success"); + await utils.viewer.me.invalidate(); + }} + onErrorMutation={() => { + showToast(t("error_updating_settings"), "error"); + }} + /> +
+ } + isCALIdentityProvider={isCALIdentityProvider} + /> + +
+ +

{t("account_deletion_cannot_be_undone")}

+
+ {/* Delete account Dialog */} + + + + + + + + <> +
+

{t("delete_account_confirmation_message")}

+ {isCALIdentityProvider && ( + + )} + + {user?.twoFactorEnabled && isCALIdentityProvider && ( +
+ + + )} + + {hasDeleteErrors && } +
+ + + + + +
+
+ + {/* If changing email, confirm password */} + + +
+
+
+ + {t("old_email_address")} + +

{user.email}

+
+
+ + {t("new_email_address")} + +

{tempFormValues?.email}

+
+
+ + + {confirmPasswordErrorMessage && } +
+ + + + +
+
+ + + + + + + + + + + + + + + + + + + {showSecondaryEmailModalOpen && ( + { + setSecondaryEmailAddErrorMessage(""); + addSecondaryEmailMutation.mutate(values); + }} + onCancel={() => { + setSecondaryEmailAddErrorMessage(""); + setShowSecondaryEmailModalOpen(false); + }} + clearErrorMessage={() => { + addSecondaryEmailMutation.reset(); + setSecondaryEmailAddErrorMessage(""); + }} + /> + )} + {!!newlyAddedSecondaryEmail && ( + setNewlyAddedSecondaryEmail(undefined)} + /> + )} + + ); +}; + +type SecondaryEmailApiPayload = { + id: number; + email: string; + isDeleted: boolean; +}; + +type ExtendedFormValues = Omit & { + secondaryEmails: SecondaryEmailApiPayload[]; +}; + +const ProfileForm = ({ + defaultValues, + onSubmit, + handleAddSecondaryEmail, + handleResendVerifyEmail, + handleAccountDisconnect, + extraField, + isPending = false, + isFallbackImg, + user, + userOrganization, + isCALIdentityProvider, +}: { + defaultValues: FormValues; + onSubmit: (values: ExtendedFormValues) => void; + handleAddSecondaryEmail: () => void; + handleResendVerifyEmail: (email: string) => void; + handleAccountDisconnect: (values: ExtendedFormValues) => void; + extraField?: React.ReactNode; + isPending: boolean; + isFallbackImg: boolean; + user: RouterOutputs["viewer"]["me"]; + userOrganization: RouterOutputs["viewer"]["me"]["organization"]; + isCALIdentityProvider: boolean; +}) => { + const { t } = useLocale(); + const [firstRender, setFirstRender] = useState(true); + + const profileFormSchema = z.object({ + username: z.string(), + avatarUrl: z.string().nullable(), + name: z + .string() + .trim() + .min(1, t("you_need_to_add_a_name")) + .max(FULL_NAME_LENGTH_MAX_LIMIT, { + message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }), + }), + email: z.string().email(), + bio: z.string(), + secondaryEmails: z.array( + z.object({ + id: z.number(), + email: z.string().email(), + emailVerified: z.union([z.string(), z.null()]).optional(), + emailPrimary: z.boolean().optional(), + }) + ), + }); + + const formMethods = useForm({ + defaultValues, + resolver: zodResolver(profileFormSchema), + }); + + const { + fields: secondaryEmailFields, + remove: deleteSecondaryEmail, + replace: updateAllSecondaryEmailFields, + } = useFieldArray({ + control: formMethods.control, + name: "secondaryEmails", + keyName: "itemId", + }); + + const getUpdatedFormValues = (values: FormValues) => { + const changedFields = formMethods.formState.dirtyFields?.secondaryEmails || []; + const updatedValues: FormValues = { + ...values, + }; + + // If the primary email is changed, we will need to update + const primaryEmailIndex = updatedValues.secondaryEmails.findIndex( + (secondaryEmail) => secondaryEmail.emailPrimary + ); + if (primaryEmailIndex >= 0) { + // Add the new updated value as primary email + updatedValues.email = updatedValues.secondaryEmails[primaryEmailIndex].email; + } + + // We will only send the emails which have already changed + const updatedEmails: Email[] = []; + changedFields.map((field, index) => { + // If the email changed and if its only secondary email, we add it for updation, the first + // item in the list is always primary email + if (field?.email && updatedValues.secondaryEmails[index]?.id) { + updatedEmails.push(updatedValues.secondaryEmails[index]); + } + }); + + const deletedEmails = (user?.secondaryEmails || []).filter( + (secondaryEmail) => !updatedValues.secondaryEmails.find((val) => val.id && val.id === secondaryEmail.id) + ); + const secondaryEmails = [ + ...updatedEmails.map((email) => ({ ...email, isDeleted: false })), + ...deletedEmails.map((email) => ({ ...email, isDeleted: true })), + ].map((secondaryEmail) => pick(secondaryEmail, ["id", "email", "isDeleted"])); + + return { + ...updatedValues, + secondaryEmails, + }; + }; + + const handleFormSubmit = (values: FormValues) => { + onSubmit(getUpdatedFormValues(values)); + }; + + const onDisconnect = () => { + handleAccountDisconnect(getUpdatedFormValues(formMethods.getValues())); + }; + + const { + formState: { isSubmitting, isDirty }, + } = formMethods; + + const isDisabled = isSubmitting || !isDirty; + return ( +
+
+
+ { + const showRemoveAvatarButton = value !== null; + return ( + <> + +
+

{t("profile_picture")}

+
+ { + onChange(newAvatar); + }} + imageSrc={getUserAvatarUrl({ avatarUrl: value })} + triggerButtonColor={showRemoveAvatarButton ? "secondary" : "primary"} + /> + + {showRemoveAvatarButton && ( + + )} +
+
+ + ); + }} + /> +
+ {extraField} +
+ +
+
+ + {secondaryEmailFields.map((field, index) => ( + { + const fields = secondaryEmailFields.map((secondaryField, cIndex) => ({ + ...secondaryField, + emailPrimary: cIndex === index, + })); + updateAllSecondaryEmailFields(fields); + }} + handleVerifyEmail={() => handleResendVerifyEmail(field.email)} + handleItemDelete={() => deleteSecondaryEmail(index)} + /> + ))} +
{t("change_email_hint")}
+ +
+
+ + md.render(formMethods.getValues("bio") || "")} + setText={(value: string) => { + formMethods.setValue("bio", turndown(value), { shouldDirty: true }); + }} + excludedToolbarItems={["blockType"]} + disableLists + firstRender={firstRender} + setFirstRender={setFirstRender} + height="80px" + /> +
+ {/* // For Non-Cal indentities, we merge the values from DB and the user logging in, + so essentially there is no point in allowing them to disconnect, since when they log in they will get logged into the same account */} + {!isCALIdentityProvider && user.email !== user.identityProviderEmail && ( +
+ +
+ {user.identityProvider.toLowerCase()} + {user.identityProviderEmail && ( + {user.identityProviderEmail} + )} +
+ +
+
+
+ )} +
+ + + +
+ ); +}; + +export default ProfileView; diff --git a/apps/web/modules/settings/security/impersonation-view.tsx b/apps/web/modules/settings/security/impersonation-view.tsx new file mode 100644 index 0000000000..3e3d8bd287 --- /dev/null +++ b/apps/web/modules/settings/security/impersonation-view.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { useState } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { showToast, SettingsToggle, SkeletonContainer, SkeletonText } from "@calcom/ui"; + +const SkeletonLoader = () => { + return ( + +
+ +
+
+ ); +}; + +const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"] }) => { + const { t } = useLocale(); + const utils = trpc.useUtils(); + const [disableImpersonation, setDisableImpersonation] = useState( + user?.disableImpersonation + ); + + const mutation = trpc.viewer.updateProfile.useMutation({ + onSuccess: () => { + showToast(t("profile_updated_successfully"), "success"); + }, + onSettled: () => { + utils.viewer.me.invalidate(); + }, + onMutate: async ({ disableImpersonation }) => { + await utils.viewer.me.cancel(); + const previousValue = utils.viewer.me.getData(); + + setDisableImpersonation(disableImpersonation); + + return { previousValue }; + }, + onError: (error, variables, context) => { + if (context?.previousValue) { + utils.viewer.me.setData(undefined, context.previousValue); + setDisableImpersonation(context.previousValue?.disableImpersonation); + } + showToast(`${t("error")}, ${error.message}`, "error"); + }, + }); + + return ( + <> +
+ { + mutation.mutate({ disableImpersonation: !checked }); + }} + switchContainerClassName="rounded-t-none border-t-0" + disabled={mutation.isPending} + /> +
+ + ); +}; + +const ProfileImpersonationViewWrapper = () => { + const { data: user, isPending } = trpc.viewer.me.useQuery(); + if (isPending || !user) return ; + + return ; +}; + +export default ProfileImpersonationViewWrapper; diff --git a/apps/web/modules/settings/security/password-view.tsx b/apps/web/modules/settings/security/password-view.tsx new file mode 100644 index 0000000000..48106836e1 --- /dev/null +++ b/apps/web/modules/settings/security/password-view.tsx @@ -0,0 +1,315 @@ +"use client"; + +import { signOut, useSession } from "next-auth/react"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; + +import { identityProviderNameMap } from "@calcom/features/auth/lib/identityProviderNameMap"; +import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; +import { classNames } from "@calcom/lib"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { IdentityProvider } from "@calcom/prisma/enums"; +import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; +import type { RouterOutputs } from "@calcom/trpc/react"; +import { trpc } from "@calcom/trpc/react"; +import { + Alert, + Button, + Form, + PasswordField, + Select, + SettingsToggle, + showToast, + SkeletonButton, + SkeletonContainer, + SkeletonText, +} from "@calcom/ui"; + +type ChangePasswordSessionFormValues = { + oldPassword: string; + newPassword: string; + sessionTimeout?: number; + apiError: string; +}; + +interface PasswordViewProps { + user: RouterOutputs["viewer"]["me"]; +} + +const SkeletonLoader = () => { + return ( + +
+ + + +
+
+ + + +
+
+ ); +}; + +const PasswordView = ({ user }: PasswordViewProps) => { + const { data } = useSession(); + const { t } = useLocale(); + const utils = trpc.useUtils(); + const metadata = userMetadataSchema.safeParse(user?.metadata); + const initialSessionTimeout = metadata.success ? metadata.data?.sessionTimeout : undefined; + + const [sessionTimeout, setSessionTimeout] = useState(initialSessionTimeout); + + const sessionMutation = trpc.viewer.updateProfile.useMutation({ + onSuccess: (data) => { + showToast(t("session_timeout_changed"), "success"); + formMethods.reset(formMethods.getValues()); + setSessionTimeout(data.metadata?.sessionTimeout); + }, + onSettled: () => { + utils.viewer.me.invalidate(); + }, + onMutate: async () => { + await utils.viewer.me.cancel(); + const previousValue = await utils.viewer.me.getData(); + const previousMetadata = userMetadataSchema.safeParse(previousValue?.metadata); + + if (previousValue && sessionTimeout && previousMetadata.success) { + utils.viewer.me.setData(undefined, { + ...previousValue, + metadata: { ...previousMetadata?.data, sessionTimeout: sessionTimeout }, + }); + return { previousValue }; + } + }, + onError: (error, _, context) => { + if (context?.previousValue) { + utils.viewer.me.setData(undefined, context.previousValue); + } + showToast(`${t("session_timeout_change_error")}, ${error.message}`, "error"); + }, + }); + const passwordMutation = trpc.viewer.auth.changePassword.useMutation({ + onSuccess: () => { + showToast(t("password_has_been_changed"), "success"); + formMethods.resetField("oldPassword"); + formMethods.resetField("newPassword"); + + if (data?.user.role === "INACTIVE_ADMIN") { + /* + AdminPasswordBanner component relies on the role returned from the session. + Next-Auth doesn't provide a way to revalidate the session cookie, + so this a workaround to hide the banner after updating the password. + discussion: https://github.com/nextauthjs/next-auth/discussions/4229 + */ + signOut({ callbackUrl: "/auth/login" }); + } + }, + onError: (error) => { + showToast(`${t("error_updating_password")}, ${t(error.message)}`, "error"); + + formMethods.setError("apiError", { + message: t(error.message), + type: "custom", + }); + }, + }); + + const createAccountPasswordMutation = trpc.viewer.auth.createAccountPassword.useMutation({ + onSuccess: () => { + showToast(t("password_reset_email", { email: user.email }), "success"); + }, + onError: (error) => { + showToast(`${t("error_creating_account_password")}, ${t(error.message)}`, "error"); + }, + }); + + const formMethods = useForm({ + defaultValues: { + oldPassword: "", + newPassword: "", + }, + }); + + const handleSubmit = (values: ChangePasswordSessionFormValues) => { + const { oldPassword, newPassword } = values; + + if (!oldPassword.length) { + formMethods.setError( + "oldPassword", + { type: "required", message: t("error_required_field") }, + { shouldFocus: true } + ); + } + + if (!newPassword.length) { + formMethods.setError( + "newPassword", + { type: "required", message: t("error_required_field") }, + { shouldFocus: true } + ); + } + + if (oldPassword && newPassword) { + passwordMutation.mutate({ oldPassword, newPassword }); + } + }; + + const timeoutOptions = [5, 10, 15].map((mins) => ({ + label: t("multiple_duration_mins", { count: mins }), + value: mins, + })); + + const isDisabled = formMethods.formState.isSubmitting || !formMethods.formState.isDirty; + + const passwordMinLength = data?.user.role === "USER" ? 7 : 15; + const isUser = data?.user.role === "USER"; + + return ( + <> + {user && user.identityProvider !== IdentityProvider.CAL && !user.passwordAdded ? ( +
+
+

+ {t("account_managed_by_identity_provider", { + provider: identityProviderNameMap[user.identityProvider], + })} +

+ +

+ {t("account_managed_by_identity_provider_description", { + provider: identityProviderNameMap[user.identityProvider], + })} +

+ +
+
+ ) : ( +
+
+ {formMethods.formState.errors.apiError && ( +
+ +
+ )} +
+
+ +
+
+ +
+
+

+ {t("invalid_password_hint", { passwordLength: passwordMinLength })} +

+
+ + + +
+ { + if (!e) { + setSessionTimeout(undefined); + + if (metadata.success) { + sessionMutation.mutate({ + metadata: { ...metadata.data, sessionTimeout: undefined }, + }); + } + } else { + setSessionTimeout(10); + } + }} + childrenClassName="lg:ml-0" + switchContainerClassName={classNames( + "py-6 px-4 sm:px-6 border-subtle rounded-xl border", + !!sessionTimeout && "rounded-b-none" + )}> + <> +
+
+

{t("session_timeout_after")}

+ { - if (event) formMethods.setValue("timeFormat", { ...event }, { shouldDirty: true }); - }} - /> - - )} - /> -
- {t("timeformat_profile_hint")} -
- ( - <> - - tmo.value === sessionTimeout) - : timeoutOptions[1] - } - isSearchable={false} - className="block h-[36px] !w-auto min-w-0 flex-none rounded-md text-sm" - onChange={(event) => { - setSessionTimeout(event?.value); - }} - /> -
-
- - - - -
-
-
- )} + ); }; -const PasswordViewWrapper = () => { - const { data: user, isPending } = trpc.viewer.me.useQuery({ includePasswordAdded: true }); - const { t } = useLocale(); - if (isPending || !user) - return ; +Page.getLayout = getLayout; +Page.PageWrapper = PageWrapper; - return ; -}; - -PasswordViewWrapper.getLayout = getLayout; -PasswordViewWrapper.PageWrapper = PageWrapper; - -export default PasswordViewWrapper; +export default Page; diff --git a/apps/web/pages/settings/security/sso.tsx b/apps/web/pages/settings/security/sso.tsx index e20f72e7cd..93cfa3bfda 100644 --- a/apps/web/pages/settings/security/sso.tsx +++ b/apps/web/pages/settings/security/sso.tsx @@ -1,9 +1,25 @@ import UserSSOView from "@calcom/features/ee/sso/page/user-sso-view"; +import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Meta } from "@calcom/ui"; -import type { CalPageWrapper } from "@components/PageWrapper"; import PageWrapper from "@components/PageWrapper"; -const Page = UserSSOView as CalPageWrapper; +const Page = () => { + const { t } = useLocale(); + + return ( + <> + + + + ); +}; Page.PageWrapper = PageWrapper; +Page.getLayout = getLayout; export default Page; diff --git a/apps/web/pages/settings/security/two-factor-auth.tsx b/apps/web/pages/settings/security/two-factor-auth.tsx index 30c4adb0cf..d098f2b272 100644 --- a/apps/web/pages/settings/security/two-factor-auth.tsx +++ b/apps/web/pages/settings/security/two-factor-auth.tsx @@ -1,102 +1,22 @@ -"use client"; - -import { useState } from "react"; - import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { trpc } from "@calcom/trpc/react"; -import { - Badge, - Meta, - SkeletonButton, - SkeletonContainer, - SkeletonText, - Alert, - SettingsToggle, -} from "@calcom/ui"; +import { Meta } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; -import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal"; -import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal"; -const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { - return ( - - -
-
- - -
-
-
- ); -}; - -const TwoFactorAuthView = () => { - const utils = trpc.useUtils(); +import TwoFactorAuthView from "~/settings/security/two-factor-auth-view"; +const Page = () => { const { t } = useLocale(); - const { data: user, isPending } = trpc.viewer.me.useQuery({ includePasswordAdded: true }); - - const [enableModalOpen, setEnableModalOpen] = useState(false); - const [disableModalOpen, setDisableModalOpen] = useState(false); - - if (isPending) - return ; - - const isCalProvider = user?.identityProvider === "CAL"; - const canSetupTwoFactor = !isCalProvider && !user?.twoFactorEnabled && !user?.passwordAdded; return ( <> - {canSetupTwoFactor && } - - user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true) - } - Badge={ - - {user?.twoFactorEnabled ? t("enabled") : t("disabled")} - - } - switchContainerClassName="rounded-t-none border-t-0" - /> - - setEnableModalOpen(!enableModalOpen)} - onEnable={() => { - setEnableModalOpen(false); - utils.viewer.me.invalidate(); - }} - onCancel={() => { - setEnableModalOpen(false); - }} - /> - - setDisableModalOpen(!disableModalOpen)} - onDisable={() => { - setDisableModalOpen(false); - utils.viewer.me.invalidate(); - }} - onCancel={() => { - setDisableModalOpen(false); - }} - /> + ); }; -TwoFactorAuthView.getLayout = getLayout; -TwoFactorAuthView.PageWrapper = PageWrapper; +Page.getLayout = getLayout; +Page.PageWrapper = PageWrapper; -export default TwoFactorAuthView; +export default Page; diff --git a/packages/features/apps/AdminAppsList.tsx b/packages/features/apps/AdminAppsList.tsx index b40c1ec91e..34749b1cd6 100644 --- a/packages/features/apps/AdminAppsList.tsx +++ b/packages/features/apps/AdminAppsList.tsx @@ -1,3 +1,5 @@ +"use client"; + import { zodResolver } from "@hookform/resolvers/zod"; // eslint-disable-next-line no-restricted-imports import { noop } from "lodash"; diff --git a/packages/features/ee/common/components/LicenseRequired.tsx b/packages/features/ee/common/components/LicenseRequired.tsx index e48af64963..4b8d23c73d 100644 --- a/packages/features/ee/common/components/LicenseRequired.tsx +++ b/packages/features/ee/common/components/LicenseRequired.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useSession } from "next-auth/react"; import type { AriaRole, ComponentType } from "react"; import React, { Fragment, useEffect } from "react"; @@ -70,9 +72,11 @@ export const withLicenseRequired = // eslint-disable-next-line react/display-name (hocProps: T) => ( - - - +
+ + + +
); export default LicenseRequired; diff --git a/packages/features/ee/organizations/pages/settings/admin-api.tsx b/packages/features/ee/organizations/pages/settings/admin-api.tsx index 6264d1506d..9c776bb06d 100644 --- a/packages/features/ee/organizations/pages/settings/admin-api.tsx +++ b/packages/features/ee/organizations/pages/settings/admin-api.tsx @@ -1,12 +1,11 @@ "use client"; import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { UpgradeTip } from "@calcom/features/tips"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Button, Icon, Meta, ButtonGroup } from "@calcom/ui"; +import { Button, Icon, ButtonGroup } from "@calcom/ui"; -const AdminAPIViewWrapper = () => { +export const AdminAPIView = () => { const { t } = useLocale(); const features = [ @@ -28,11 +27,6 @@ const AdminAPIViewWrapper = () => { ]; return ( -
{ ); }; -AdminAPIViewWrapper.getLayout = getLayout; - -export default AdminAPIViewWrapper; +export default AdminAPIView; diff --git a/packages/features/ee/organizations/pages/settings/admin/AdminOrgEditPage.tsx b/packages/features/ee/organizations/pages/settings/admin/AdminOrgEditPage.tsx index 3a22b2af31..6ead4803ae 100644 --- a/packages/features/ee/organizations/pages/settings/admin/AdminOrgEditPage.tsx +++ b/packages/features/ee/organizations/pages/settings/admin/AdminOrgEditPage.tsx @@ -1,44 +1,14 @@ +"use client"; + import type { Team } from "@prisma/client"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; -import { z } from "zod"; +import type { z } from "zod"; -import NoSSR from "@calcom/core/components/NoSSR"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; import type { orgSettingsSchema } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc/react"; -import { Button, Form, Meta, TextField, showToast } from "@calcom/ui"; - -import { getLayout } from "../../../../../settings/layouts/SettingsLayout"; -import LicenseRequired from "../../../../common/components/LicenseRequired"; - -const paramsSchema = z.object({ id: z.coerce.number() }); - -const OrgEditPage = () => { - const params = useParamsWithFallback(); - const parsedParams = paramsSchema.safeParse(params); - - if (!parsedParams.success) return
Invalid id
; - - return ; -}; - -const OrgEditView = ({ orgId }: { orgId: number }) => { - const [org] = trpc.viewer.organizations.adminGet.useSuspenseQuery({ id: orgId }); - - return ( - - - - - - - ); -}; +import { Button, Form, TextField, showToast } from "@calcom/ui"; type FormValues = { name: Team["name"]; @@ -46,7 +16,7 @@ type FormValues = { organizationSettings: z.infer; }; -const OrgForm = ({ +export const OrgForm = ({ org, }: { org: FormValues & { @@ -108,6 +78,4 @@ const OrgForm = ({ ); }; -OrgEditPage.getLayout = getLayout; - -export default OrgEditPage; +export default OrgForm; diff --git a/packages/features/ee/organizations/pages/settings/admin/AdminOrgPage.tsx b/packages/features/ee/organizations/pages/settings/admin/AdminOrgPage.tsx index 0e42e9230b..d931de362a 100644 --- a/packages/features/ee/organizations/pages/settings/admin/AdminOrgPage.tsx +++ b/packages/features/ee/organizations/pages/settings/admin/AdminOrgPage.tsx @@ -3,39 +3,18 @@ import { Trans } from "next-i18next"; import { useState } from "react"; -import NoSSR from "@calcom/core/components/NoSSR"; -import LicenseRequired from "@calcom/ee/common/components/LicenseRequired"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { - Badge, - ConfirmationDialogContent, - Dialog, - DropdownActions, - Meta, - showToast, - Table, -} from "@calcom/ui"; +import { Badge, ConfirmationDialogContent, Dialog, DropdownActions, showToast, Table } from "@calcom/ui"; -import { getLayout } from "../../../../../settings/layouts/SettingsLayout"; import { subdomainSuffix } from "../../../../organizations/lib/orgDomains"; const { Body, Cell, ColumnTitle, Header, Row } = Table; -function AdminOrgTable() { +export function AdminOrgTable() { const { t } = useLocale(); const utils = trpc.useUtils(); const [data] = trpc.viewer.organizations.adminGetAll.useSuspenseQuery(); - const verifyMutation = trpc.viewer.organizations.adminVerify.useMutation({ - onSuccess: async (_data, variables) => { - showToast(t("org_has_been_processed"), "success"); - await invalidateQueries(utils, variables); - }, - onError: (err) => { - console.error(err.message); - showToast(t("org_error_processing"), "error"); - }, - }); const updateMutation = trpc.viewer.organizations.adminUpdate.useMutation({ onSuccess: async (_data, variables) => { showToast(t("org_has_been_processed"), "success"); @@ -240,21 +219,7 @@ function AdminOrgTable() { ); } -const AdminOrgList = () => { - const { t } = useLocale(); - return ( - - - - - - - ); -}; - -AdminOrgList.getLayout = getLayout; - -export default AdminOrgList; +export default AdminOrgTable; const DeleteOrgDialog = ({ org, diff --git a/packages/features/ee/sso/page/user-sso-view.tsx b/packages/features/ee/sso/page/user-sso-view.tsx index 84b3d6bf8c..11566247ec 100644 --- a/packages/features/ee/sso/page/user-sso-view.tsx +++ b/packages/features/ee/sso/page/user-sso-view.tsx @@ -5,9 +5,7 @@ import { useEffect } from "react"; import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { Meta } from "@calcom/ui"; -import { getLayout } from "../../../settings/layouts/SettingsLayout"; import SSOConfiguration from "../components/SSOConfiguration"; const SAMLSSO = () => { @@ -22,16 +20,9 @@ const SAMLSSO = () => { return (
-
); }; -SAMLSSO.getLayout = getLayout; - export default SAMLSSO; diff --git a/packages/features/ee/users/components/UsersTable.tsx b/packages/features/ee/users/components/UsersTable.tsx index fc8bc52545..291d95008f 100644 --- a/packages/features/ee/users/components/UsersTable.tsx +++ b/packages/features/ee/users/components/UsersTable.tsx @@ -150,7 +150,7 @@ function UsersTableBare() { const [userToDelete, setUserToDelete] = useState(null); return ( - <> +
)} - +
); } diff --git a/packages/features/ee/users/pages/users-add-view.tsx b/packages/features/ee/users/pages/users-add-view.tsx index 9ebc493b05..01d67ad759 100644 --- a/packages/features/ee/users/pages/users-add-view.tsx +++ b/packages/features/ee/users/pages/users-add-view.tsx @@ -4,9 +4,8 @@ import { usePathname, useRouter } from "next/navigation"; import { getParserWithGeneric } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc/react"; -import { Meta, showToast } from "@calcom/ui"; +import { showToast } from "@calcom/ui"; -import { getLayout } from "../../../settings/layouts/SettingsLayout"; import LicenseRequired from "../../common/components/LicenseRequired"; import { UserForm } from "../components/UserForm"; import { userBodySchema } from "../schemas/userBodySchema"; @@ -30,20 +29,19 @@ const UsersAddView = () => { }, }); return ( - - - { - const parser = getParserWithGeneric(userBodySchema); - const parsedValues = parser(values); - mutation.mutate(parsedValues); - }} - /> - +
+ + { + const parser = getParserWithGeneric(userBodySchema); + const parsedValues = parser(values); + mutation.mutate(parsedValues); + }} + /> + +
); }; -UsersAddView.getLayout = getLayout; - export default UsersAddView; diff --git a/packages/features/ee/users/pages/users-edit-view.tsx b/packages/features/ee/users/pages/users-edit-view.tsx index 00ed0d7b92..c214607493 100644 --- a/packages/features/ee/users/pages/users-edit-view.tsx +++ b/packages/features/ee/users/pages/users-edit-view.tsx @@ -7,13 +7,14 @@ import NoSSR from "@calcom/core/components/NoSSR"; import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback"; import { getParserWithGeneric } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc/react"; -import { Meta, showToast } from "@calcom/ui"; +import { showToast } from "@calcom/ui"; -import { getLayout } from "../../../settings/layouts/SettingsLayout"; import LicenseRequired from "../../common/components/LicenseRequired"; import { UserForm } from "../components/UserForm"; import { userBodySchema } from "../schemas/userBodySchema"; +import type { UserAdminRouterOutputs } from "../server/trpc-router"; +type User = UserAdminRouterOutputs["get"]["user"]; const userIdSchema = z.object({ id: z.coerce.number() }); const UsersEditPage = () => { @@ -22,14 +23,22 @@ const UsersEditPage = () => { if (!input.success) return
Invalid input
; - return ; + const [data] = trpc.viewer.users.get.useSuspenseQuery({ userId: input.data.id }); + const { user } = data; + + return ( + + + + + + ); }; -const UsersEditView = ({ userId }: { userId: number }) => { +export const UsersEditView = ({ user }: { user: User }) => { const pathname = usePathname(); const router = useRouter(); - const [data] = trpc.viewer.users.get.useSuspenseQuery({ userId }); - const { user } = data; + const utils = trpc.useUtils(); const mutation = trpc.viewer.users.update.useMutation({ onSuccess: async () => { @@ -43,29 +52,22 @@ const UsersEditView = ({ userId }: { userId: number }) => { }, }); return ( - - - - { - const parser = getParserWithGeneric(userBodySchema); - const parsedValues = parser(values); - const data: Partial = { - ...parsedValues, - userId: user.id, - }; - // Don't send username if it's the same as the current one - if (user.username === data.username) delete data.username; - mutation.mutate(data); - }} - defaultValues={user} - /> - - + { + const parser = getParserWithGeneric(userBodySchema); + const parsedValues = parser(values); + const data: Partial = { + ...parsedValues, + userId: user.id, + }; + // Don't send username if it's the same as the current one + if (user.username === data.username) delete data.username; + mutation.mutate(data); + }} + defaultValues={user} + /> ); }; -UsersEditPage.getLayout = getLayout; - -export default UsersEditPage; +export default UsersEditView; diff --git a/packages/features/ee/users/pages/users-listing-view.tsx b/packages/features/ee/users/pages/users-listing-view.tsx index 51b8dfa229..ee9fd5038e 100644 --- a/packages/features/ee/users/pages/users-listing-view.tsx +++ b/packages/features/ee/users/pages/users-listing-view.tsx @@ -1,7 +1,6 @@ "use client"; import NoSSR from "@calcom/core/components/NoSSR"; -import { Button, Meta } from "@calcom/ui"; import { getLayout } from "../../../settings/layouts/SettingsLayout"; import { UsersTable } from "../components/UsersTable"; @@ -9,17 +8,6 @@ import { UsersTable } from "../components/UsersTable"; const DeploymentUsersListPage = () => { return ( <> - - {/* TODO: Add import users functionality */} - {/* */} - -
- } - /> diff --git a/packages/features/flags/pages/flag-listing-view.tsx b/packages/features/flags/pages/flag-listing-view.tsx index 1bd0c3443e..23f08ed768 100644 --- a/packages/features/flags/pages/flag-listing-view.tsx +++ b/packages/features/flags/pages/flag-listing-view.tsx @@ -1,7 +1,9 @@ +"use client"; + import { Suspense } from "react"; import NoSSR from "@calcom/core/components/NoSSR"; -import { Meta, SkeletonText, SkeletonContainer } from "@calcom/ui"; +import { SkeletonText, SkeletonContainer } from "@calcom/ui"; import { FlagAdminList } from "../components/FlagAdminList"; @@ -18,13 +20,10 @@ const SkeletonLoader = () => { export const FlagListingView = () => { return ( - <> - - - }> - - - - + + }> + + + ); }; diff --git a/packages/features/settings/appDir/SettingsHeader.tsx b/packages/features/settings/appDir/SettingsHeader.tsx new file mode 100644 index 0000000000..8299b0d7fc --- /dev/null +++ b/packages/features/settings/appDir/SettingsHeader.tsx @@ -0,0 +1,57 @@ +import React, { Suspense } from "react"; + +import { classNames } from "@calcom/lib"; +import { Icon } from "@calcom/ui"; + +interface HeaderProps { + children: React.ReactNode; + title?: string; + description?: string; + CTA?: React.ReactNode; + borderInShellHeader?: boolean; + backButton?: boolean; +} + +export default function Header({ + children, + title, + description, + CTA, + borderInShellHeader, + backButton, +}: HeaderProps) { + return ( +
+
+
+ {backButton && ( + + + + )} +
+ {title ? ( +

+ {title} +

+ ) : ( +
+ )} + {description ? ( +

{description}

+ ) : ( +
+ )} +
+
{CTA}
+
+
+ }>{children} +
+ ); +} diff --git a/packages/features/settings/layouts/SettingsLayoutAppDir.tsx b/packages/features/settings/appDir/SettingsLayoutAppDir.tsx similarity index 62% rename from packages/features/settings/layouts/SettingsLayoutAppDir.tsx rename to packages/features/settings/appDir/SettingsLayoutAppDir.tsx index c20fdbfd70..d96b7f7972 100644 --- a/packages/features/settings/layouts/SettingsLayoutAppDir.tsx +++ b/packages/features/settings/appDir/SettingsLayoutAppDir.tsx @@ -5,7 +5,7 @@ import { useSession } from "next-auth/react"; import Link from "next/link"; import { usePathname, useRouter } from "next/navigation"; import type { ComponentProps } from "react"; -import React, { Suspense, useEffect, useState } from "react"; +import React, { useEffect, useState, useMemo } from "react"; import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; import Shell from "@calcom/features/shell/Shell"; @@ -18,7 +18,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale"; import { IdentityProvider, MembershipRole, UserPermissionRole } from "@calcom/prisma/enums"; import { trpc } from "@calcom/trpc/react"; import type { VerticalTabItemProps } from "@calcom/ui"; -import { Badge, Button, ErrorBoundary, Icon, Skeleton, useMeta, VerticalTabItem } from "@calcom/ui"; +import { Badge, Button, ErrorBoundary, Icon, Skeleton, VerticalTabItem } from "@calcom/ui"; const tabs: VerticalTabItemProps[] = [ { @@ -31,6 +31,7 @@ const tabs: VerticalTabItemProps[] = [ { name: "calendars", href: "/settings/my-account/calendars" }, { name: "conferencing", href: "/settings/my-account/conferencing" }, { name: "appearance", href: "/settings/my-account/appearance" }, + { name: "out_of_office", href: "/settings/my-account/out-of-office" }, // TODO // { name: "referrals", href: "/settings/my-account/referrals" }, ], @@ -59,6 +60,7 @@ const tabs: VerticalTabItemProps[] = [ // { name: "webhooks", href: "/settings/developer/webhooks" }, { name: "api_keys", href: "/settings/developer/api-keys" }, + { name: "admin_api", href: "/settings/organizations/admin-api" }, // TODO: Add profile level for embeds // { name: "embeds", href: "/v2/settings/developer/embeds" }, ], @@ -80,13 +82,26 @@ const tabs: VerticalTabItemProps[] = [ href: "/settings/organizations/members", }, { - name: "appearance", - href: "/settings/organizations/appearance", + name: "privacy", + href: "/settings/organizations/privacy", }, { name: "billing", href: "/settings/organizations/billing", }, + { name: "OAuth Clients", href: "/settings/organizations/platform/oauth-clients" }, + { + name: "SSO", + href: "/settings/organizations/sso", + }, + { + name: "directory_sync", + href: "/settings/organizations/dsync", + }, + { + name: "admin_api", + href: "https://cal.com/docs/enterprise-features/api/api-reference/bookings#admin-access", + }, ], }, { @@ -95,6 +110,12 @@ const tabs: VerticalTabItemProps[] = [ icon: "users", children: [], }, + { + name: "other_teams", + href: "/settings/organizations/teams/other", + icon: "users", + children: [], + }, { name: "admin", href: "/settings/admin", @@ -114,57 +135,92 @@ const tabs: VerticalTabItemProps[] = [ ]; tabs.find((tab) => { - // Add "SAML SSO" to the tab if (tab.name === "security" && !HOSTED_CAL_FEATURES) { tab.children?.push({ name: "sso_configuration", href: "/settings/security/sso" }); + // TODO: Enable dsync for self hosters + // tab.children?.push({ name: "directory_sync", href: "/settings/security/dsync" }); } }); // The following keys are assigned to admin only const adminRequiredKeys = ["admin"]; const organizationRequiredKeys = ["organization"]; +const organizationAdminKeys = ["privacy", "billing", "OAuth Clients", "SSO", "directory_sync"]; const useTabs = () => { const session = useSession(); - const { data: user } = trpc.viewer.me.useQuery(); + const { data: user } = trpc.viewer.me.useQuery({ includePasswordAdded: true }); const orgBranding = useOrgBranding(); - const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN; + const isOrgAdminOrOwner = + orgBranding?.role === MembershipRole.ADMIN || orgBranding?.role === MembershipRole.OWNER; - tabs.map((tab) => { - if (tab.href === "/settings/my-account") { - tab.name = user?.name || "my_account"; - tab.icon = undefined; - tab.avatar = getUserAvatarUrl(user); - } else if (tab.href === "/settings/organizations") { - tab.name = orgBranding?.name || "organization"; - tab.avatar = `${orgBranding?.fullDomain}/org/${orgBranding?.slug}/avatar.png`; - } else if ( - tab.href === "/settings/security" && - user?.identityProvider === IdentityProvider.GOOGLE && - !user?.twoFactorEnabled - ) { - tab.children = tab?.children?.filter( - (childTab) => childTab.href !== "/settings/security/two-factor-auth" - ); - } - return tab; - }); + const processTabsMemod = useMemo(() => { + const processedTabs = tabs.map((tab) => { + if (tab.href === "/settings/my-account") { + return { + ...tab, + name: user?.name || "my_account", + icon: undefined, + avatar: getUserAvatarUrl(user), + }; + } else if (tab.href === "/settings/organizations") { + const newArray = (tab?.children ?? []).filter( + (child) => isOrgAdminOrOwner || !organizationAdminKeys.includes(child.name) + ); - // check if name is in adminRequiredKeys - return tabs.filter((tab) => { - if (organizationRequiredKeys.includes(tab.name)) return !!session.data?.user?.org; + // TODO: figure out feature flag as it doesnt cause a re-render of the component when loaded. + // You have to refresh the page to see the changes. + if (true) { + newArray.splice(4, 0, { + name: "attributes", + href: "/settings/organizations/attributes", + }); + } - if (isAdmin) return true; - return !adminRequiredKeys.includes(tab.name); - }); + return { + ...tab, + children: newArray, + name: orgBranding?.name || "organization", + avatar: getPlaceholderAvatar(orgBranding?.logoUrl, orgBranding?.name), + }; + } else if ( + tab.href === "/settings/security" && + user?.identityProvider === IdentityProvider.GOOGLE && + !user?.twoFactorEnabled && + !user?.passwordAdded + ) { + const filtered = tab?.children?.filter( + (childTab) => childTab.href !== "/settings/security/two-factor-auth" + ); + return { ...tab, children: filtered }; + } else if (tab.href === "/settings/developer") { + const filtered = tab?.children?.filter( + (childTab) => isOrgAdminOrOwner || childTab.name !== "admin_api" + ); + return { ...tab, children: filtered }; + } + return tab; + }); + + // check if name is in adminRequiredKeys + return processedTabs.filter((tab) => { + if (organizationRequiredKeys.includes(tab.name)) return !!orgBranding; + if (tab.name === "other_teams" && !isOrgAdminOrOwner) return false; + + if (isAdmin) return true; + return !adminRequiredKeys.includes(tab.name); + }); + }, [isAdmin, orgBranding, isOrgAdminOrOwner, user]); + + return processTabsMemod; }; const BackButtonInSidebar = ({ name }: { name: string }) => { return ( { - const searchParams = useCompatSearchParams(); +const TeamListCollapsible = () => { + const { data: teams } = trpc.viewer.teams.list.useQuery(); const { t } = useLocale(); - const tabsWithPermissions = useTabs(); const [teamMenuState, setTeamMenuState] = useState<{ teamId: number | undefined; teamMenuOpen: boolean }[]>(); - const [otherTeamMenuState, setOtherTeamMenuState] = useState< - { - teamId: number | undefined; - teamMenuOpen: boolean; - }[] - >(); - const { data: teams } = trpc.viewer.teams.list.useQuery(); - const session = useSession(); - const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { - enabled: !!session.data?.user?.org, - }); - - const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(); - + const searchParams = useCompatSearchParams(); useEffect(() => { if (teams) { const teamStates = teams?.map((team) => ({ @@ -223,6 +261,147 @@ const SettingsSidebarContainer = ({ } }, [searchParams?.get("id"), teams]); + return ( + <> + {teams && + teamMenuState && + teams.map((team, index: number) => { + if (!teamMenuState[index]) { + return null; + } + if (teamMenuState.some((teamState) => teamState.teamId === team.id)) + return ( + + setTeamMenuState([ + ...teamMenuState, + (teamMenuState[index] = { + ...teamMenuState[index], + teamMenuOpen: !teamMenuState[index].teamMenuOpen, + }), + ]) + }> + +
+ setTeamMenuState([ + ...teamMenuState, + (teamMenuState[index] = { + ...teamMenuState[index], + teamMenuOpen: !teamMenuState[index].teamMenuOpen, + }), + ]) + }> +
+ {teamMenuState[index].teamMenuOpen ? ( + + ) : ( + + )} +
+ {!team.parentId && ( + {team.name + )} +

{team.name}

+ {!team.accepted && ( + + Inv. + + )} +
+
+ + {team.accepted && ( + + )} + + + {(team.role === MembershipRole.OWNER || + team.role === MembershipRole.ADMIN || + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore this exists wtf? + (team.isOrgAdmin && team.isOrgAdmin)) && ( + <> + {/* TODO */} + {/* */} + + {/* Hide if there is a parent ID */} + {!team.parentId ? ( + <> + + + ) : null} + + )} + +
+ ); + })} + + ); +}; + +const SettingsSidebarContainer = ({ + className = "", + navigationIsOpenedOnMobile, + bannersHeight, +}: SettingsSidebarContainerProps) => { + const searchParams = useCompatSearchParams(); + const { t } = useLocale(); + const tabsWithPermissions = useTabs(); + const [otherTeamMenuState, setOtherTeamMenuState] = useState< + { + teamId: number | undefined; + teamMenuOpen: boolean; + }[] + >(); + const session = useSession(); + const { data: currentOrg } = trpc.viewer.organizations.listCurrent.useQuery(undefined, { + enabled: !!session.data?.user?.org, + }); + + const { data: otherTeams } = trpc.viewer.organizations.listOtherTeams.useQuery(undefined, { + enabled: !!session.data?.user?.org, + }); + // Same as above but for otherTeams useEffect(() => { if (otherTeams) { @@ -244,17 +423,6 @@ const SettingsSidebarContainer = ({ const isOrgAdminOrOwner = currentOrg && currentOrg?.user?.role && ["OWNER", "ADMIN"].includes(currentOrg?.user?.role); - if (isOrgAdminOrOwner) { - const teamsIndex = tabsWithPermissions.findIndex((tab) => tab.name === "teams"); - - tabsWithPermissions.splice(teamsIndex + 1, 0, { - name: "other_teams", - href: "/settings/organizations/teams/other", - icon: "users", - children: [], - }); - } - return (