From 602ed87d0f78a162a7e4ef1ec3f5fcce1d375a8b Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Tue, 23 Apr 2024 23:35:02 +0530 Subject: [PATCH 01/78] add endpoint to fetch managed user from client id --- .../oauth-clients/oauth-clients.controller.ts | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts index a0dd99e1227f5..903ee106635e3 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller.ts @@ -3,12 +3,17 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator"; import { Roles } from "@/modules/auth/decorators/roles/roles.decorator"; import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard"; import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard"; +import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output"; import { CreateOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/CreateOAuthClientResponse.dto"; -import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto"; +import { + GetOAuthClientResponseDto, + GetOAuthClientManagedUsersResponseDto, +} from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto"; import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto"; import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input"; import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository"; import { UserWithProfile } from "@/modules/users/users.repository"; +import { UsersRepository } from "@/modules/users/users.repository"; import { Body, Controller, @@ -30,6 +35,7 @@ import { ApiCreatedResponse as DocsCreatedResponse, } from "@nestjs/swagger"; import { MembershipRole } from "@prisma/client"; +import { User } from "@prisma/client"; import { SUCCESS_STATUS } from "@calcom/platform-constants"; import { CreateOAuthClientInput } from "@calcom/platform-types"; @@ -47,7 +53,10 @@ Second, make sure that the logged in user has organizationId set to pass the Org export class OAuthClientsController { private readonly logger = new Logger("OAuthClientController"); - constructor(private readonly oauthClientRepository: OAuthClientRepository) {} + constructor( + private readonly oauthClientRepository: OAuthClientRepository, + private readonly userRepository: UsersRepository + ) {} @Post("/") @HttpCode(HttpStatus.CREATED) @@ -98,6 +107,21 @@ export class OAuthClientsController { return { status: SUCCESS_STATUS, data: client }; } + @Get("/managed-users/:clientId") + @HttpCode(HttpStatus.OK) + @Roles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]) + @DocsOperation({ description: AUTH_DOCUMENTATION }) + async getOAuthClientManagedUsersById( + @Param("clientId") clientId: string + ): Promise { + const existingManagedUsers = await this.userRepository.findManagedUsersByOAuthClientId(clientId, 0, 50); // second argument is for offset while third is for limit + + if (!existingManagedUsers) { + throw new NotFoundException(`OAuth client with ID ${clientId} does not have any managed users`); + } + return { status: SUCCESS_STATUS, data: existingManagedUsers.map((user) => this.getResponseUser(user)) }; + } + @Patch("/:clientId") @HttpCode(HttpStatus.OK) @Roles([MembershipRole.ADMIN, MembershipRole.OWNER]) @@ -120,4 +144,17 @@ export class OAuthClientsController { const client = await this.oauthClientRepository.deleteOAuthClient(clientId); return { status: SUCCESS_STATUS, data: client }; } + + private getResponseUser(user: User): ManagedUserOutput { + return { + id: user.id, + email: user.email, + username: user.username, + timeZone: user.timeZone, + weekStart: user.weekStart, + createdDate: user.createdDate, + timeFormat: user.timeFormat, + defaultScheduleId: user.defaultScheduleId, + }; + } } From ec41c6831a8fb5daffa97047c4e2c86f8501ea5c Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Tue, 23 Apr 2024 23:45:48 +0530 Subject: [PATCH 02/78] update typings --- .../responses/GetOAuthClientResponse.dto.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto.ts b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto.ts index e95fd60155f7a..fcc60ae3efba1 100644 --- a/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto.ts +++ b/apps/api/v2/src/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientResponse.dto.ts @@ -1,3 +1,4 @@ +import { ManagedUserOutput } from "@/modules/oauth-clients/controllers/oauth-client-users/outputs/managed-user.output"; import { ApiProperty } from "@nestjs/swagger"; import { Type } from "class-transformer"; import { @@ -62,3 +63,17 @@ export class GetOAuthClientResponseDto { @Type(() => PlatformOAuthClientDto) data!: PlatformOAuthClientDto; } + +export class GetOAuthClientManagedUsersResponseDto { + @ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] }) + @IsEnum([SUCCESS_STATUS, ERROR_STATUS]) + status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS; + + @ApiProperty({ + type: ManagedUserOutput, + }) + @IsNotEmptyObject() + @ValidateNested() + @Type(() => ManagedUserOutput) + data!: ManagedUserOutput[]; +} From da31c118df1b525b4f63650fa5a8323bf4c7090f Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Tue, 23 Apr 2024 23:49:35 +0530 Subject: [PATCH 03/78] minor tweaks --- .../platform/oauth-clients/OAuthClientCard.tsx | 4 +--- .../platform/oauth-clients/OAuthClientForm.tsx | 9 ++------- 2 files changed, 3 insertions(+), 10 deletions(-) diff --git a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx b/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx index f57f6b5e6575c..a1b2db8d407df 100644 --- a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx +++ b/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx @@ -38,8 +38,6 @@ export const OAuthClientCard = ({ isLoading, areEmailsEnabled, }: OAuthClientCardProps) => { - console.log(areEmailsEnabled); - const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => { let permissionsMessage = ""; const hasReadPermission = hasPermission(permissions, value.read); @@ -114,7 +112,7 @@ export const OAuthClientCard = ({
Permissions: -
{clientPermissions}
+ {permissions ?
{clientPermissions}
: <> Disabled}
Redirect uris: diff --git a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientForm.tsx b/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientForm.tsx index 09be35225e944..0c4d7039e1588 100644 --- a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientForm.tsx +++ b/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientForm.tsx @@ -6,7 +6,7 @@ import { useForm, useFieldArray } from "react-hook-form"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions"; import { showToast } from "@calcom/ui"; -import { Meta, Button, TextField, Label, Tooltip } from "@calcom/ui"; +import { Button, TextField, Label, Tooltip } from "@calcom/ui"; import { useCreateOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient"; @@ -62,7 +62,7 @@ export const OAuthClientForm: FC = () => { const { mutateAsync, isPending } = useCreateOAuthClient({ onSuccess: () => { showToast("OAuth client created successfully", "success"); - router.push("/settings/organizations/platform/oauth-clients"); + router.push("/settings/platform/"); }, onError: () => { showToast("Internal server error, please try again later", "error"); @@ -132,11 +132,6 @@ export const OAuthClientForm: FC = () => { return (
-
From 7bc4a6b96fba95f8f2d0528b304606ebe1249946 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Tue, 23 Apr 2024 23:56:46 +0530 Subject: [PATCH 04/78] custom hook to fetch managed users from client id --- .../platform/oauth-clients/useOAuthClients.ts | 31 ++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/apps/web/lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients.ts b/apps/web/lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients.ts index 6295ca7579601..0b0a84da365ad 100644 --- a/apps/web/lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients.ts +++ b/apps/web/lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients.ts @@ -3,6 +3,17 @@ import { useQuery } from "@tanstack/react-query"; import type { ApiSuccessResponse } from "@calcom/platform-types"; import type { PlatformOAuthClient } from "@calcom/prisma/client"; +type ManagedUser = { + id: number; + email: string; + username: string | null; + timeZone: string; + weekStart: string; + createdDate: Date; + timeFormat: number | null; + defaultScheduleId: number | null; +}; + export const useOAuthClients = () => { const query = useQuery>({ queryKey: ["oauth-clients"], @@ -23,7 +34,7 @@ export const useOAuthClient = (clientId: string) => { error, data: response, } = useQuery>({ - queryKey: ["oauth-client"], + queryKey: ["oauth-client", clientId], queryFn: () => { return fetch(`/api/v2/oauth-clients/${clientId}`, { method: "get", @@ -34,3 +45,21 @@ export const useOAuthClient = (clientId: string) => { return { isLoading, error, data: response?.data }; }; +export const useGetOAuthClientManagedUsers = (clientId: string) => { + const { + isLoading, + error, + data: response, + refetch, + } = useQuery>({ + queryKey: ["oauth-client-managed-users", clientId], + queryFn: () => { + return fetch(`/api/v2/oauth-clients/managed-users/${clientId}`, { + method: "get", + headers: { "Content-type": "application/json" }, + }).then((res) => res.json()); + }, + }); + + return { isLoading, error, data: response?.data, refetch }; +}; From fc59a7f08b1b9ab8b4dd117e4d7eb20ea60b9fb1 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Tue, 23 Apr 2024 23:58:09 +0530 Subject: [PATCH 05/78] add translations for platform onboarding --- apps/web/public/static/locales/en/common.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 25280e65cda37..40f943cac95e5 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2028,12 +2028,16 @@ "organization_banner_description": "Create an environments where your teams can create shared apps, workflows and event types with round-robin and collective scheduling.", "organization_banner_title": "Manage organizations with multiple teams", "set_up_your_organization": "Set up your organization", + "set_up_your_platform_organization": "Set up your platform", "organizations_description": "Organizations are shared environments where teams can create shared event types, apps, workflows and more.", + "platform_organization_description": "Cal.com Platform lets you integrate scheduling effortlessly into your app using platform apis and atoms.", "must_enter_organization_name": "Must enter an organization name", "must_enter_organization_admin_email": "Must enter your organization email address", "admin_email": "Your organization email address", + "platform_admin_email": "Your admin email address", "admin_username": "Administrator's username", "organization_name": "Organization name", + "platform_name": "Platform name", "organization_url": "Organization URL", "organization_verify_header": "Verify your organization email", "organization_verify_email_body": "Please use the code below to verify your email address to continue setting up your organization.", From bbf2c8cad209f5a164e2f31eb45f5175f6fd0db1 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 24 Apr 2024 00:01:20 +0530 Subject: [PATCH 06/78] add isPlatformOrg boolean to figure out which is platform and which is not --- .../components/CreateANewOrganizationForm.tsx | 86 +++++++++++-------- 1 file changed, 52 insertions(+), 34 deletions(-) diff --git a/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx b/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx index 39001bd04fa77..1a308c9e77d50 100644 --- a/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx +++ b/packages/features/ee/organizations/components/CreateANewOrganizationForm.tsx @@ -24,15 +24,21 @@ function extractDomainFromEmail(email: string) { return out.split(".")[0]; } -export const CreateANewOrganizationForm = () => { +export const CreateANewOrganizationForm = ({ isPlatformOrg = false }: { isPlatformOrg?: boolean }) => { const session = useSession(); if (!session.data) { return null; } - return ; + return ; }; -const CreateANewOrganizationFormChild = ({ session }: { session: Ensure }) => { +const CreateANewOrganizationFormChild = ({ + session, + isPlatformOrg = false, +}: { + session: Ensure; + isPlatformOrg?: boolean; +}) => { const { t } = useLocale(); const router = useRouter(); const telemetry = useTelemetry(); @@ -46,11 +52,13 @@ const CreateANewOrganizationFormChild = ({ session }: { session: Ensure({ defaultValues: { slug: !isAdmin ? deriveSlugFromEmail(defaultOrgOwnerEmail) : undefined, orgOwnerEmail: !isAdmin ? defaultOrgOwnerEmail : undefined, name: !isAdmin ? deriveOrgNameFromEmail(defaultOrgOwnerEmail) : undefined, + isPlatform: false, }, }); @@ -66,10 +74,14 @@ const CreateANewOrganizationFormChild = ({ session }: { session: Ensure { if (err.message === "organization_url_taken") { @@ -94,7 +106,11 @@ const CreateANewOrganizationFormChild = ({ session }: { session: Ensure { if (!createOrganizationMutation.isPending) { setServerErrorMessage(null); - createOrganizationMutation.mutate(v); + createOrganizationMutation.mutate({ + ...v, + isPlatform: isPlatformOrg ? true : false, + slug: isPlatformOrg ? v.name.toLocaleLowerCase() : v.slug, + }); } }}>
@@ -116,7 +132,7 @@ const CreateANewOrganizationFormChild = ({ session }: { session: Ensure { const email = e?.target.value; @@ -147,7 +163,7 @@ const CreateANewOrganizationFormChild = ({ session }: { session: Ensure { newOrganizationFormMethods.setValue("name", e?.target.value.trim()); @@ -162,33 +178,35 @@ const CreateANewOrganizationFormChild = ({ session }: { session: Ensure
-
- ( - { - newOrganizationFormMethods.setValue("slug", slugify(e?.target.value), { - shouldTouch: true, - }); - newOrganizationFormMethods.clearErrors("slug"); - }} - /> - )} - /> -
+ {!isPlatformOrg && ( +
+ ( + { + newOrganizationFormMethods.setValue("slug", slugify(e?.target.value), { + shouldTouch: true, + }); + newOrganizationFormMethods.clearErrors("slug"); + }} + /> + )} + /> +
+ )} - {(isAdmin || isImpersonated) && ( + {(isAdmin || isImpersonated) && !isPlatformOrg && ( <>
From 78d3823c37de20f00d9d0493e3ed1173f96a5ae6 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 24 Apr 2024 00:12:12 +0530 Subject: [PATCH 07/78] set isPlatform hook based on data obtained from org --- packages/lib/server/repository/organization.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/lib/server/repository/organization.ts b/packages/lib/server/repository/organization.ts index 45b0c9188634a..eee5382f17b6c 100644 --- a/packages/lib/server/repository/organization.ts +++ b/packages/lib/server/repository/organization.ts @@ -53,6 +53,7 @@ export class OrganizationRepository { orgPricePerSeat: orgData.pricePerSeat, isPlatform: orgData.isPlatform, }, + isPlatform: orgData.isPlatform, }, }); From b23985683ee39f00788757e5688698ffc630afed Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 24 Apr 2024 00:13:31 +0530 Subject: [PATCH 08/78] add limitWidth prop to control component width --- packages/ui/components/empty-screen/EmptyScreen.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/ui/components/empty-screen/EmptyScreen.tsx b/packages/ui/components/empty-screen/EmptyScreen.tsx index 9f29648d3ab46..75f26869b36aa 100644 --- a/packages/ui/components/empty-screen/EmptyScreen.tsx +++ b/packages/ui/components/empty-screen/EmptyScreen.tsx @@ -21,6 +21,7 @@ export function EmptyScreen({ className, iconClassName, iconWrapperClassName, + limitWidth = true, }: { Icon?: IconName; customIcon?: React.ReactElement; @@ -34,6 +35,7 @@ export function EmptyScreen({ dashedBorder?: boolean; iconWrapperClassName?: string; iconClassName?: string; + limitWidth?: boolean; } & React.HTMLAttributes) { return ( <> @@ -62,7 +64,7 @@ export function EmptyScreen({
)} {!customIcon ? null : <>{customIcon}} -
+

Date: Wed, 24 Apr 2024 00:17:31 +0530 Subject: [PATCH 09/78] add props to shell to make sidebar display different tabs based on if the shell isPlattform or not --- packages/features/shell/Shell.tsx | 210 +++++++++++++++++++----------- 1 file changed, 134 insertions(+), 76 deletions(-) diff --git a/packages/features/shell/Shell.tsx b/packages/features/shell/Shell.tsx index 90de661d1064d..9ab4baf2af058 100644 --- a/packages/features/shell/Shell.tsx +++ b/packages/features/shell/Shell.tsx @@ -287,7 +287,7 @@ const Layout = (props: LayoutProps) => { {props.SidebarContainer ? ( cloneElement(props.SidebarContainer, { bannersHeight }) ) : ( - + )}
@@ -328,6 +328,7 @@ export type LayoutProps = { afterHeading?: ReactNode; smallHeading?: boolean; hideHeadingOnMobile?: boolean; + isPlatformUser?: boolean; }; const useAppTheme = () => { @@ -669,27 +670,72 @@ const navigation: NavigationItemType[] = [ }, ]; -const moreSeparatorIndex = navigation.findIndex((item) => item.name === MORE_SEPARATOR_NAME); -// We create all needed navigation items for the different use cases -const { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems } = navigation.reduce< - Record ->( - (items, item, index) => { - // We filter out the "more" separator in` desktop navigation - if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item); - // Items for mobile bottom navigation - if (index < moreSeparatorIndex + 1 && !item.onlyDesktop) { - items.mobileNavigationBottomItems.push(item); - } // Items for the "more" menu in mobile navigation - else { - items.mobileNavigationMoreItems.push(item); - } - return items; +const platformNavigation: NavigationItemType[] = [ + { + name: "Dashboard", + href: "/settings/platform/", + icon: "layout-dashboard", + }, + { + name: "Platform docs", + href: "https://docs.cal.com/docs/platform", + icon: "bar-chart", + target: "_blank", + }, + { + name: "API reference", + href: "https://api.cal.com/v2/docs#/", + icon: "terminal", + target: "_blank", + }, + { + name: "Cal.com atoms", + href: "https://docs.cal.com/docs/platform#atoms", + icon: "atom", + target: "_blank", + }, + { + name: "Examples app", + href: "https://platform.cal.dev/", + icon: "code", + target: "_blank", }, - { desktopNavigationItems: [], mobileNavigationBottomItems: [], mobileNavigationMoreItems: [] } -); + { + name: MORE_SEPARATOR_NAME, + href: "https://docs.cal.com/docs/platform/faq", + icon: "ellipsis", + target: "_blank", + }, +]; + +const getDesktopNavigationItems = (isPlatformNavigation = false) => { + const navigationType = !isPlatformNavigation ? navigation : platformNavigation; + const moreSeparatorIndex = navigationType.findIndex((item) => item.name === MORE_SEPARATOR_NAME); + + const { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems } = ( + !isPlatformNavigation ? navigation : platformNavigation + ).reduce>( + (items, item, index) => { + // We filter out the "more" separator in` desktop navigation + if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item); + // Items for mobile bottom navigation + if (index < moreSeparatorIndex + 1 && !item.onlyDesktop) { + items.mobileNavigationBottomItems.push(item); + } // Items for the "more" menu in mobile navigation + else { + items.mobileNavigationMoreItems.push(item); + } + return items; + }, + { desktopNavigationItems: [], mobileNavigationBottomItems: [], mobileNavigationMoreItems: [] } + ); + + return { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMoreItems }; +}; + +const Navigation = ({ isPlatformNavigation = false }: { isPlatformNavigation?: boolean }) => { + const { desktopNavigationItems } = getDesktopNavigationItems(isPlatformNavigation); -const Navigation = () => { return (
- {/* logo icon for tablet */} - - +

-
- - {bottomNavItems.map((item, index) => ( - - - {!!item.icon && ( - - - ))} - {!IS_VISUAL_REGRESSION_TESTING && } -
+ {!isPlatformUser && ( +
+ + {bottomNavItems.map((item, index) => ( + + + {!!item.icon && ( + + + ))} + {!IS_VISUAL_REGRESSION_TESTING && } +
+ )}
); @@ -1096,7 +1147,10 @@ export function ShellMain(props: LayoutProps) { } function MainContainer({ - MobileNavigationContainer: MobileNavigationContainerProp = , + isPlatformUser, + MobileNavigationContainer: MobileNavigationContainerProp = ( + + ), TopNavContainer: TopNavContainerProp = , ...props }: LayoutProps) { @@ -1149,13 +1203,17 @@ function TopNav() { ); } -export const MobileNavigationMoreItems = () => ( -
    - {mobileNavigationMoreItems.map((item) => ( - - ))} -
-); +export const MobileNavigationMoreItems = () => { + const { mobileNavigationMoreItems } = getDesktopNavigationItems(); + + return ( +
    + {mobileNavigationMoreItems.map((item) => ( + + ))} +
+ ); +}; function ProfileDropdown() { const { update, data: sessionData } = useSession(); From 51f56b01d3ab1069f866d03419a06a90f1d3ca65 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 24 Apr 2024 00:18:49 +0530 Subject: [PATCH 10/78] platform related pages --- apps/web/pages/settings/platform/index.tsx | 240 ++++++++++++++++++ .../web/pages/settings/platform/new/index.tsx | 45 ++++ .../platform/oauth-clients/create.tsx | 28 ++ 3 files changed, 313 insertions(+) create mode 100644 apps/web/pages/settings/platform/index.tsx create mode 100644 apps/web/pages/settings/platform/new/index.tsx create mode 100644 apps/web/pages/settings/platform/oauth-clients/create.tsx diff --git a/apps/web/pages/settings/platform/index.tsx b/apps/web/pages/settings/platform/index.tsx new file mode 100644 index 0000000000000..11b1cbb762420 --- /dev/null +++ b/apps/web/pages/settings/platform/index.tsx @@ -0,0 +1,240 @@ +import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; +import { useRouter } from "next/router"; +import { useState, useEffect } from "react"; + +import Shell from "@calcom/features/shell/Shell"; +import { + showToast, + EmptyScreen, + Button, + Dropdown, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, + DropdownItem, +} from "@calcom/ui"; + +import { + useOAuthClients, + useGetOAuthClientManagedUsers, +} from "@lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients"; +import { useDeleteOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient"; +import useMeQuery from "@lib/hooks/useMeQuery"; + +import PageWrapper from "@components/PageWrapper"; +import { OAuthClientCard } from "@components/settings/organizations/platform/oauth-clients/OAuthClientCard"; + +const queryClient = new QueryClient(); +// middleware.ts +export default function Platform() { + const { data, isLoading: isOAuthClientLoading, refetch: refetchClients } = useOAuthClients(); + const [initialClientId, setInitialClientId] = useState(""); + const [initialClientName, setInitialClientName] = useState(""); + const { + isLoading: isManagedUserLoading, + data: managedUserData, + refetch: refetchManagedUsers, + } = useGetOAuthClientManagedUsers(initialClientId); + + console.log("these are all the managed users of this particular client id"); + console.log(managedUserData?.map((user) => console.log(user))); + + const { mutateAsync, isPending: isDeleting } = useDeleteOAuthClient({ + onSuccess: () => { + showToast("OAuth client deleted successfully", "success"); + refetchClients(); + refetchManagedUsers(); + }, + }); + + const handleDelete = async (id: string) => { + await mutateAsync({ id: id }); + }; + const { data: user, isLoading } = useMeQuery(); + const isPlatformUser = user?.organization.isPlatform; + + useEffect(() => { + setInitialClientId(data[0]?.id); + setInitialClientName(data[0]?.name); + }, [data]); + + const NewOAuthClientButton = () => { + const router = useRouter(); + + return ( + + ); + }; + + if (isLoading || isOAuthClientLoading) return
Loading...
; + + if (isPlatformUser) { + return ( + +
+ +
+
+
+

+ OAuth Clients +

+

+ Connect your platform to cal.com with OAuth +

+
+
+ +
+
+ {Array.isArray(data) && data.length ? ( + <> +
+ {data.map((client, index) => { + return ( + + ); + })} +
+ + ) : ( + } + /> + )} +
+
+
+
+

+ Managed Users +

+

+ See all the managed users created by your OAuth client. +

+
+ {Array.isArray(data) && data.length && ( +
+ + + + + + {data.map((user) => { + return ( +
+ {initialClientName !== user.name ? ( + + { + setInitialClientId(user.id); + setInitialClientName(user.name); + refetchManagedUsers(); + }}> + {user.name} + + + ) : ( + <> + )} +
+ ); + })} +
+
+
+ )} +
+ {Array.isArray(managedUserData) && !isManagedUserLoading && managedUserData.length ? ( + <> + + + + + + + + {managedUserData.map((user) => { + return ( + + + + + + ); + })} +
IdUsernameEmail
+ {user.id} + {user.username} + {user.email} +
+ + ) : ( + + )} +
+
+
+
+ ); + } + + return ( +
+ }> + You are not subscribed to a Platform plan. + +
+ ); +} + +Platform.PageWrapper = PageWrapper; diff --git a/apps/web/pages/settings/platform/new/index.tsx b/apps/web/pages/settings/platform/new/index.tsx new file mode 100644 index 0000000000000..6f644fad0a980 --- /dev/null +++ b/apps/web/pages/settings/platform/new/index.tsx @@ -0,0 +1,45 @@ +"use client"; + +import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired"; +import { CreateANewOrganizationForm } from "@calcom/features/ee/organizations/components"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { WizardLayout, Meta, WizardLayoutAppDir } from "@calcom/ui"; + +import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps"; + +import PageWrapper from "@components/PageWrapper"; + +const CreateNewOrganizationPage = () => { + const { t } = useLocale(); + return ( + + + + + ); +}; +const LayoutWrapper = (page: React.ReactElement) => { + return ( + + {page} + + ); +}; + +export const LayoutWrapperAppDir = (page: React.ReactElement) => { + return ( + + {page} + + ); +}; + +CreateNewOrganizationPage.getLayout = LayoutWrapper; +CreateNewOrganizationPage.PageWrapper = PageWrapper; + +export default CreateNewOrganizationPage; + +export { getServerSideProps }; diff --git a/apps/web/pages/settings/platform/oauth-clients/create.tsx b/apps/web/pages/settings/platform/oauth-clients/create.tsx new file mode 100644 index 0000000000000..cdfc5bb58f354 --- /dev/null +++ b/apps/web/pages/settings/platform/oauth-clients/create.tsx @@ -0,0 +1,28 @@ +import Shell from "@calcom/features/shell/Shell"; + +import PageWrapper from "@components/PageWrapper"; +import { OAuthClientForm } from "@components/settings/organizations/platform/oauth-clients/OAuthClientForm"; + +export default function CreateOAuthClient() { + return ( +
+ +
+
+
+

+ OAuth client creation form +

+

+ This is the form to create a new OAuth client +

+
+
+ +
+
+
+ ); +} + +CreateOAuthClient.PageWrapper = PageWrapper; From 15c51890d5be4795dbedadd4eab78e0428f37102 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 24 Apr 2024 01:04:00 +0530 Subject: [PATCH 11/78] fix merge conflicts --- .../platform/oauth-clients/OAuthClientCard.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx b/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx index a1b2db8d407df..0d7fa83532ad6 100644 --- a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx +++ b/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx @@ -1,3 +1,4 @@ +import { useRouter } from "next/navigation"; import React from "react"; import { classNames } from "@calcom/lib"; @@ -38,6 +39,8 @@ export const OAuthClientCard = ({ isLoading, areEmailsEnabled, }: OAuthClientCardProps) => { + const router = useRouter(); + const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => { let permissionsMessage = ""; const hasReadPermission = hasPermission(permissions, value.read); @@ -112,7 +115,7 @@ export const OAuthClientCard = ({
Permissions: - {permissions ?
{clientPermissions}
: <> Disabled} +
{clientPermissions}
Redirect uris: @@ -137,7 +140,14 @@ export const OAuthClientCard = ({ Emails enabled: {areEmailsEnabled ? "Yes" : "No"}
-
+
+
@@ -219,6 +299,7 @@ export const OAuthClientForm: FC = () => { label="Booking redirect uri" className="w-[100%]" {...register("bookingRedirectUri")} + disabled={disabledForm} />
@@ -229,6 +310,7 @@ export const OAuthClientForm: FC = () => { label="Booking cancel redirect uri" className="w-[100%]" {...register("bookingCancelRedirectUri")} + disabled={disabledForm} /> @@ -239,6 +321,7 @@ export const OAuthClientForm: FC = () => { label="Booking reschedule redirect uri" className="w-[100%]" {...register("bookingRescheduleRedirectUri")} + disabled={disabledForm} /> @@ -248,22 +331,27 @@ export const OAuthClientForm: FC = () => { id="areEmailsEnabled" className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed" type="checkbox" + disabled={disabledForm} /> +
-

Permissions

-
{permissionsCheckboxes}
- From 793efbf67fdb54536b11853e9b0c850544873cd5 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 24 Apr 2024 01:44:08 +0530 Subject: [PATCH 13/78] platform oauth client form and card --- .../oauth-clients/OAuthClientCard.tsx | 151 ++++++++++ .../oauth-clients/OAuthClientForm.tsx | 271 ++++++++++++++++++ 2 files changed, 422 insertions(+) create mode 100644 apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx create mode 100644 apps/web/components/settings/platform/oauth-clients/OAuthClientForm.tsx diff --git a/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx b/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx new file mode 100644 index 0000000000000..2a8c7cf40517d --- /dev/null +++ b/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx @@ -0,0 +1,151 @@ +import React from "react"; + +import { classNames } from "@calcom/lib"; +import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants"; +import type { Avatar } from "@calcom/prisma/client"; +import { Button, Icon, showToast } from "@calcom/ui"; + +import { hasPermission } from "../../../../../../packages/platform/utils/permissions"; + +type OAuthClientCardProps = { + name: string; + logo?: Avatar; + redirectUris: string[]; + bookingRedirectUri: string | null; + bookingCancelRedirectUri: string | null; + bookingRescheduleRedirectUri: string | null; + areEmailsEnabled: boolean; + permissions: number; + lastItem: boolean; + id: string; + secret: string; + onDelete: (id: string) => Promise; + isLoading: boolean; +}; + +export const OAuthClientCard = ({ + name, + logo, + redirectUris, + bookingRedirectUri, + bookingCancelRedirectUri, + bookingRescheduleRedirectUri, + permissions, + id, + secret, + lastItem, + onDelete, + isLoading, + areEmailsEnabled, +}: OAuthClientCardProps) => { + const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => { + let permissionsMessage = ""; + const hasReadPermission = hasPermission(permissions, value.read); + const hasWritePermission = hasPermission(permissions, value.write); + + if (hasReadPermission || hasWritePermission) { + permissionsMessage = hasReadPermission ? "read" : "write"; + } + + if (hasReadPermission && hasWritePermission) { + permissionsMessage = "read/write"; + } + + return ( + !!permissionsMessage && ( +
+  {permissionsMessage} {`${value.label}s`.toLocaleLowerCase()} + {Object.values(PERMISSIONS_GROUPED_MAP).length === index + 1 ? " " : ", "} +
+ ) + ); + }); + + return ( +
+
+
+

+ Client name: {name} +

+
+ {!!logo && ( +
+ <>{logo} +
+ )} +
+
+
Client Id:
+
{id}
+ { + navigator.clipboard.writeText(id); + showToast("Client id copied to clipboard.", "success"); + }} + /> +
+
+
+
Client Secret:
+
+ {[...new Array(20)].map((_, index) => ( + + ))} + { + navigator.clipboard.writeText(secret); + showToast("Client secret copied to clipboard.", "success"); + }} + /> +
+
+
+ Permissions: + {permissions ?
{clientPermissions}
: <> Disabled} +
+
+ Redirect uris: + {redirectUris.map((item, index) => (redirectUris.length === index + 1 ? `${item}` : `${item}, `))} +
+ {bookingRedirectUri && ( +
+ Booking redirect uri: {bookingRedirectUri} +
+ )} + {bookingRescheduleRedirectUri && ( +
+ Booking reschedule uri: {bookingRescheduleRedirectUri} +
+ )} + {bookingCancelRedirectUri && ( +
+ Booking cancel uri: {bookingCancelRedirectUri} +
+ )} +
+ Emails enabled: {areEmailsEnabled ? "Yes" : "No"} +
+
+
+ +
+
+ ); +}; diff --git a/apps/web/components/settings/platform/oauth-clients/OAuthClientForm.tsx b/apps/web/components/settings/platform/oauth-clients/OAuthClientForm.tsx new file mode 100644 index 0000000000000..0c4d7039e1588 --- /dev/null +++ b/apps/web/components/settings/platform/oauth-clients/OAuthClientForm.tsx @@ -0,0 +1,271 @@ +import { useRouter } from "next/router"; +import type { FC } from "react"; +import React, { useState, useCallback } from "react"; +import { useForm, useFieldArray } from "react-hook-form"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions"; +import { showToast } from "@calcom/ui"; +import { Button, TextField, Label, Tooltip } from "@calcom/ui"; + +import { useCreateOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient"; + +type FormValues = { + name: string; + logo?: string; + permissions: number; + eventTypeRead: boolean; + eventTypeWrite: boolean; + bookingRead: boolean; + bookingWrite: boolean; + scheduleRead: boolean; + scheduleWrite: boolean; + appsRead: boolean; + appsWrite: boolean; + profileRead: boolean; + profileWrite: boolean; + redirectUris: { + uri: string; + }[]; + bookingRedirectUri?: string; + bookingCancelRedirectUri?: string; + bookingRescheduleRedirectUri?: string; + areEmailsEnabled?: boolean; +}; + +export const OAuthClientForm: FC = () => { + const { t } = useLocale(); + const router = useRouter(); + const { register, control, handleSubmit, setValue } = useForm({ + defaultValues: { + redirectUris: [{ uri: "" }], + }, + }); + const { fields, append, remove } = useFieldArray({ + control, + name: "redirectUris", + }); + const [isSelectAllPermissionsChecked, setIsSelectAllPermissionsChecked] = useState(false); + + const selectAllPermissions = useCallback(() => { + Object.keys(PERMISSIONS_GROUPED_MAP).forEach((key) => { + const entity = key as keyof typeof PERMISSIONS_GROUPED_MAP; + const permissionKey = PERMISSIONS_GROUPED_MAP[entity].key; + + setValue(`${permissionKey}Read`, !isSelectAllPermissionsChecked); + setValue(`${permissionKey}Write`, !isSelectAllPermissionsChecked); + }); + + setIsSelectAllPermissionsChecked((preValue) => !preValue); + }, [isSelectAllPermissionsChecked, setValue]); + + const { mutateAsync, isPending } = useCreateOAuthClient({ + onSuccess: () => { + showToast("OAuth client created successfully", "success"); + router.push("/settings/platform/"); + }, + onError: () => { + showToast("Internal server error, please try again later", "error"); + }, + }); + + const onSubmit = (data: FormValues) => { + let userPermissions = 0; + const userRedirectUris = data.redirectUris.map((uri) => uri.uri).filter((uri) => !!uri); + + Object.keys(PERMISSIONS_GROUPED_MAP).forEach((key) => { + const entity = key as keyof typeof PERMISSIONS_GROUPED_MAP; + const entityKey = PERMISSIONS_GROUPED_MAP[entity].key; + const read = PERMISSIONS_GROUPED_MAP[entity].read; + const write = PERMISSIONS_GROUPED_MAP[entity].write; + if (data[`${entityKey}Read`]) userPermissions |= read; + if (data[`${entityKey}Write`]) userPermissions |= write; + }); + + mutateAsync({ + name: data.name, + permissions: userPermissions, + // logo: data.logo, + redirectUris: userRedirectUris, + bookingRedirectUri: data.bookingRedirectUri, + bookingCancelRedirectUri: data.bookingCancelRedirectUri, + bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri, + areEmailsEnabled: data.areEmailsEnabled, + }); + }; + + const permissionsCheckboxes = Object.keys(PERMISSIONS_GROUPED_MAP).map((key) => { + const entity = key as keyof typeof PERMISSIONS_GROUPED_MAP; + const permissionKey = PERMISSIONS_GROUPED_MAP[entity].key; + const permissionLabel = PERMISSIONS_GROUPED_MAP[entity].label; + + return ( +
+

{permissionLabel}

+
+
+ + +
+
+ + +
+
+
+ ); + }); + + return ( +
+
+
+ +
+
+ + {fields.map((field, index) => { + return ( +
+
+ +
+
+
+
+ ); + })} +
+ {/**
+ ( + <> + +
+ } + size="sm" + /> +
+ { + setValue("logo", newAvatar); + }} + /> +
+
+ + )} + /> +
*/} +
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+
+

Permissions

+ +
+
{permissionsCheckboxes}
+
+ +
+
+ ); +}; From b4a5724c6d1dde84b19ece0b091290ae8549b6d6 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 24 Apr 2024 16:45:22 +0530 Subject: [PATCH 14/78] remove everything related to platform from organization --- .../oauth-clients/OAuthClientCard.tsx | 161 -------- .../oauth-clients/OAuthClientForm.tsx | 359 ------------------ .../platform/oauth-clients/create.tsx | 22 -- .../platform/oauth-clients/index.tsx | 103 ----- 4 files changed, 645 deletions(-) delete mode 100644 apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx delete mode 100644 apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientForm.tsx delete mode 100644 apps/web/pages/settings/organizations/platform/oauth-clients/create.tsx delete mode 100644 apps/web/pages/settings/organizations/platform/oauth-clients/index.tsx diff --git a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx b/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx deleted file mode 100644 index 0d7fa83532ad6..0000000000000 --- a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientCard.tsx +++ /dev/null @@ -1,161 +0,0 @@ -import { useRouter } from "next/navigation"; -import React from "react"; - -import { classNames } from "@calcom/lib"; -import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants"; -import type { Avatar } from "@calcom/prisma/client"; -import { Button, Icon, showToast } from "@calcom/ui"; - -import { hasPermission } from "../../../../../../../packages/platform/utils/permissions"; - -type OAuthClientCardProps = { - name: string; - logo?: Avatar; - redirectUris: string[]; - bookingRedirectUri: string | null; - bookingCancelRedirectUri: string | null; - bookingRescheduleRedirectUri: string | null; - areEmailsEnabled: boolean; - permissions: number; - lastItem: boolean; - id: string; - secret: string; - onDelete: (id: string) => Promise; - isLoading: boolean; -}; - -export const OAuthClientCard = ({ - name, - logo, - redirectUris, - bookingRedirectUri, - bookingCancelRedirectUri, - bookingRescheduleRedirectUri, - permissions, - id, - secret, - lastItem, - onDelete, - isLoading, - areEmailsEnabled, -}: OAuthClientCardProps) => { - const router = useRouter(); - - const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => { - let permissionsMessage = ""; - const hasReadPermission = hasPermission(permissions, value.read); - const hasWritePermission = hasPermission(permissions, value.write); - - if (hasReadPermission || hasWritePermission) { - permissionsMessage = hasReadPermission ? "read" : "write"; - } - - if (hasReadPermission && hasWritePermission) { - permissionsMessage = "read/write"; - } - - return ( - !!permissionsMessage && ( -
-  {permissionsMessage} {`${value.label}s`.toLocaleLowerCase()} - {Object.values(PERMISSIONS_GROUPED_MAP).length === index + 1 ? " " : ", "} -
- ) - ); - }); - - return ( -
-
-
-

- Client name: {name} -

-
- {!!logo && ( -
- <>{logo} -
- )} -
-
-
Client Id:
-
{id}
- { - navigator.clipboard.writeText(id); - showToast("Client id copied to clipboard.", "success"); - }} - /> -
-
-
-
Client Secret:
-
- {[...new Array(20)].map((_, index) => ( - - ))} - { - navigator.clipboard.writeText(secret); - showToast("Client secret copied to clipboard.", "success"); - }} - /> -
-
-
- Permissions: -
{clientPermissions}
-
-
- Redirect uris: - {redirectUris.map((item, index) => (redirectUris.length === index + 1 ? `${item}` : `${item}, `))} -
- {bookingRedirectUri && ( -
- Booking redirect uri: {bookingRedirectUri} -
- )} - {bookingRescheduleRedirectUri && ( -
- Booking reschedule uri: {bookingRescheduleRedirectUri} -
- )} - {bookingCancelRedirectUri && ( -
- Booking cancel uri: {bookingCancelRedirectUri} -
- )} -
- Emails enabled: {areEmailsEnabled ? "Yes" : "No"} -
-
-
- - -
-
- ); -}; diff --git a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientForm.tsx b/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientForm.tsx deleted file mode 100644 index 7225896d9d2f3..0000000000000 --- a/apps/web/components/settings/organizations/platform/oauth-clients/OAuthClientForm.tsx +++ /dev/null @@ -1,359 +0,0 @@ -import { useRouter } from "next/navigation"; -import type { FC } from "react"; -import React, { useState, useCallback, useEffect } from "react"; -import { useForm, useFieldArray } from "react-hook-form"; - -import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions"; -import { showToast } from "@calcom/ui"; -import { Meta, Button, TextField, Label, Tooltip } from "@calcom/ui"; - -import { useOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients"; -import { - useCreateOAuthClient, - useUpdateOAuthClient, -} from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient"; - -import { - hasAppsReadPermission, - hasAppsWritePermission, - hasBookingReadPermission, - hasBookingWritePermission, - hasEventTypeReadPermission, - hasEventTypeWritePermission, - hasProfileReadPermission, - hasProfileWritePermission, - hasScheduleReadPermission, - hasScheduleWritePermission, -} from "../../../../../../../packages/platform/utils/permissions"; - -type FormValues = { - name: string; - logo?: string; - permissions: number; - eventTypeRead: boolean; - eventTypeWrite: boolean; - bookingRead: boolean; - bookingWrite: boolean; - scheduleRead: boolean; - scheduleWrite: boolean; - appsRead: boolean; - appsWrite: boolean; - profileRead: boolean; - profileWrite: boolean; - redirectUris: { - uri: string; - }[]; - bookingRedirectUri?: string; - bookingCancelRedirectUri?: string; - bookingRescheduleRedirectUri?: string; - areEmailsEnabled?: boolean; -}; - -export const OAuthClientForm: FC<{ clientId?: string }> = ({ clientId }) => { - const { t } = useLocale(); - const router = useRouter(); - const { data, isFetched, isError, refetch } = useOAuthClient(clientId); - const { register, control, handleSubmit, setValue } = useForm({ - defaultValues: { - redirectUris: [{ uri: "" }], - }, - }); - const { fields, append, remove } = useFieldArray({ - control, - name: "redirectUris", - }); - useEffect(() => { - if (isFetched && data && !isError) { - setValue("name", data.name); - data.bookingRedirectUri && setValue("bookingRedirectUri", data.bookingRedirectUri); - data.bookingCancelRedirectUri && setValue("bookingCancelRedirectUri", data.bookingCancelRedirectUri); - data.bookingRescheduleRedirectUri && - setValue("bookingRescheduleRedirectUri", data.bookingRescheduleRedirectUri); - setValue("areEmailsEnabled", data?.areEmailsEnabled); - data?.redirectUris.forEach((uri: string, index: number) => { - index === 0 && setValue(`redirectUris.${index}.uri`, uri); - index !== 0 && append({ uri }); - }); - if (hasAppsReadPermission(data.permissions)) setValue("appsRead", true); - if (hasAppsWritePermission(data.permissions)) setValue("appsWrite", true); - if (hasBookingReadPermission(data.permissions)) setValue("bookingRead", true); - if (hasBookingWritePermission(data.permissions)) setValue("bookingWrite", true); - if (hasEventTypeReadPermission(data.permissions)) setValue("eventTypeRead", true); - if (hasEventTypeWritePermission(data.permissions)) setValue("eventTypeWrite", true); - if (hasProfileReadPermission(data.permissions)) setValue("profileRead", true); - if (hasProfileWritePermission(data.permissions)) setValue("profileWrite", true); - if (hasScheduleReadPermission(data.permissions)) setValue("scheduleRead", true); - if (hasScheduleWritePermission(data.permissions)) setValue("scheduleWrite", true); - } - }, [isFetched, data]); - const disabledForm = Boolean(clientId && !isFetched && isError); - - const [isSelectAllPermissionsChecked, setIsSelectAllPermissionsChecked] = useState(false); - - const selectAllPermissions = useCallback(() => { - Object.keys(PERMISSIONS_GROUPED_MAP).forEach((key) => { - const entity = key as keyof typeof PERMISSIONS_GROUPED_MAP; - const permissionKey = PERMISSIONS_GROUPED_MAP[entity].key; - - setValue(`${permissionKey}Read`, !isSelectAllPermissionsChecked); - setValue(`${permissionKey}Write`, !isSelectAllPermissionsChecked); - }); - - setIsSelectAllPermissionsChecked((preValue) => !preValue); - }, [isSelectAllPermissionsChecked, setValue]); - - const { mutateAsync: save, isPending: isSaving } = useCreateOAuthClient({ - onSuccess: () => { - showToast("OAuth client created successfully", "success"); - refetch(); - router.push("/settings/organizations/platform/oauth-clients"); - }, - onError: () => { - showToast("Internal server error, please try again later", "error"); - }, - }); - const { mutateAsync: update, isPending: isUpdating } = useUpdateOAuthClient({ - onSuccess: () => { - showToast("OAuth client updated successfully", "success"); - refetch(); - router.push("/settings/organizations/platform/oauth-clients"); - }, - onError: () => { - showToast("Internal server error, please try again later", "error"); - }, - clientId, - }); - - const onSubmit = (data: FormValues) => { - let userPermissions = 0; - const userRedirectUris = data.redirectUris.map((uri) => uri.uri).filter((uri) => !!uri); - - Object.keys(PERMISSIONS_GROUPED_MAP).forEach((key) => { - const entity = key as keyof typeof PERMISSIONS_GROUPED_MAP; - const entityKey = PERMISSIONS_GROUPED_MAP[entity].key; - const read = PERMISSIONS_GROUPED_MAP[entity].read; - const write = PERMISSIONS_GROUPED_MAP[entity].write; - if (data[`${entityKey}Read`]) userPermissions |= read; - if (data[`${entityKey}Write`]) userPermissions |= write; - }); - - if (clientId) { - // don't update permissions if client is already created - update({ - name: data.name, - // logo: data.logo, - redirectUris: userRedirectUris, - bookingRedirectUri: data.bookingRedirectUri, - bookingCancelRedirectUri: data.bookingCancelRedirectUri, - bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri, - areEmailsEnabled: data.areEmailsEnabled, - }); - } else { - save({ - name: data.name, - permissions: userPermissions, - // logo: data.logo, - redirectUris: userRedirectUris, - bookingRedirectUri: data.bookingRedirectUri, - bookingCancelRedirectUri: data.bookingCancelRedirectUri, - bookingRescheduleRedirectUri: data.bookingRescheduleRedirectUri, - areEmailsEnabled: data.areEmailsEnabled, - }); - } - }; - const isPending = isSaving || isUpdating; - - const permissionsCheckboxes = Object.keys(PERMISSIONS_GROUPED_MAP).map((key) => { - const entity = key as keyof typeof PERMISSIONS_GROUPED_MAP; - const permissionKey = PERMISSIONS_GROUPED_MAP[entity].key; - const permissionLabel = PERMISSIONS_GROUPED_MAP[entity].label; - - return ( -
-

{permissionLabel}

-
-
- - -
-
- - -
-
-
- ); - }); - - return ( -
- -
-
- -
-
- - {fields.map((field, index) => { - return ( -
-
- -
-
-
-
- ); - })} -
- {/**
- ( - <> - -
- } - size="sm" - /> -
- { - setValue("logo", newAvatar); - }} - /> -
-
- - )} - /> -
*/} -
- - - -
-
- - - -
-
- - - -
-
- - -
- -
-
- -

Permissions

-
- -
-
{permissionsCheckboxes}
-
- - -
-
- ); -}; diff --git a/apps/web/pages/settings/organizations/platform/oauth-clients/create.tsx b/apps/web/pages/settings/organizations/platform/oauth-clients/create.tsx deleted file mode 100644 index 3d26f40440c72..0000000000000 --- a/apps/web/pages/settings/organizations/platform/oauth-clients/create.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import React from "react"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams"; - -import PageWrapper from "@components/PageWrapper"; -import { OAuthClientForm } from "@components/settings/organizations/platform/oauth-clients/OAuthClientForm"; - -export const CreateOAuthClient = () => { - const searchParams = useCompatSearchParams(); - const clientId = searchParams?.get("clientId") || ""; - return ( -
- -
- ); -}; - -CreateOAuthClient.getLayout = getLayout; -CreateOAuthClient.PageWrapper = PageWrapper; - -export default CreateOAuthClient; diff --git a/apps/web/pages/settings/organizations/platform/oauth-clients/index.tsx b/apps/web/pages/settings/organizations/platform/oauth-clients/index.tsx deleted file mode 100644 index 8daef7c847284..0000000000000 --- a/apps/web/pages/settings/organizations/platform/oauth-clients/index.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { QueryClientProvider, QueryClient } from "@tanstack/react-query"; -import { useRouter } from "next/navigation"; -import React from "react"; - -import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; -import { EmptyScreen, showToast } from "@calcom/ui"; -import { Meta, Button } from "@calcom/ui"; -import { Spinner } from "@calcom/ui/components/icon/Spinner"; - -import { useOAuthClients } from "@lib/hooks/settings/organizations/platform/oauth-clients/useOAuthClients"; -import { useDeleteOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient"; - -import PageWrapper from "@components/PageWrapper"; -import { OAuthClientCard } from "@components/settings/organizations/platform/oauth-clients/OAuthClientCard"; - -const queryClient = new QueryClient(); - -export const OAuthClients = () => { - const { data, isLoading, refetch: refetchClients } = useOAuthClients(); - const { mutateAsync, isPending: isDeleting } = useDeleteOAuthClient({ - onSuccess: () => { - showToast("OAuth client deleted successfully", "success"); - refetchClients(); - }, - }); - - const handleDelete = async (id: string) => { - await mutateAsync({ id: id }); - }; - - const NewOAuthClientButton = () => { - const router = useRouter(); - - return ( - - ); - }; - - if (isLoading) { - return ; - } - - return ( - -
- } - borderInShellHeader={true} - /> -
- {Array.isArray(data) && data.length ? ( - <> -
- {data.map((client, index) => { - return ( - - ); - })} -
- - ) : ( - } - /> - )} -
-
-
- ); -}; - -OAuthClients.getLayout = getLayout; -OAuthClients.PageWrapper = PageWrapper; - -export default OAuthClients; From fca0cf7e22514b68dececcb5d597646033033cf5 Mon Sep 17 00:00:00 2001 From: Ryukemeister Date: Wed, 24 Apr 2024 16:48:13 +0530 Subject: [PATCH 15/78] update oauth client card and form --- .../oauth-clients/OAuthClientCard.tsx | 12 +- .../oauth-clients/OAuthClientForm.tsx | 118 +++++++++++++++--- 2 files changed, 111 insertions(+), 19 deletions(-) diff --git a/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx b/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx index 2a8c7cf40517d..521ab3d3adc82 100644 --- a/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx +++ b/apps/web/components/settings/platform/oauth-clients/OAuthClientCard.tsx @@ -1,3 +1,4 @@ +import { useRouter } from "next/navigation"; import React from "react"; import { classNames } from "@calcom/lib"; @@ -38,6 +39,8 @@ export const OAuthClientCard = ({ isLoading, areEmailsEnabled, }: OAuthClientCardProps) => { + const router = useRouter(); + const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => { let permissionsMessage = ""; const hasReadPermission = hasPermission(permissions, value.read); @@ -137,7 +140,14 @@ export const OAuthClientCard = ({ Emails enabled: {areEmailsEnabled ? "Yes" : "No"} -
+
+
@@ -219,6 +297,7 @@ export const OAuthClientForm: FC = () => { label="Booking redirect uri" className="w-[100%]" {...register("bookingRedirectUri")} + disabled={disabledForm} />
@@ -229,6 +308,7 @@ export const OAuthClientForm: FC = () => { label="Booking cancel redirect uri" className="w-[100%]" {...register("bookingCancelRedirectUri")} + disabled={disabledForm} /> @@ -239,6 +319,7 @@ export const OAuthClientForm: FC = () => { label="Booking reschedule redirect uri" className="w-[100%]" {...register("bookingRescheduleRedirectUri")} + disabled={disabledForm} /> @@ -248,6 +329,7 @@ export const OAuthClientForm: FC = () => { id="areEmailsEnabled" className="bg-default border-default h-4 w-4 shrink-0 cursor-pointer rounded-[4px] border ring-offset-2 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed" type="checkbox" + disabled={disabledForm} />