feat: update platform oauth client form (#14542)

* feat: update platform oauth client form

* fixup! feat: update platform oauth client form

* add tooltip to mention permissions cannot be modified

---------

Co-authored-by: Ryukemeister <sahalrajiv-extc@atharvacoe.ac.in>
This commit is contained in:
Morgan
2024-04-16 01:55:00 +01:00
committed by GitHub
co-authored by Ryukemeister
parent 9684aa3ad5
commit 8e883a6b10
7 changed files with 197 additions and 30 deletions
@@ -1,3 +1,4 @@
import { useRouter } from "next/navigation";
import React from "react";
import { classNames } from "@calcom/lib";
@@ -38,7 +39,7 @@ export const OAuthClientCard = ({
isLoading,
areEmailsEnabled,
}: OAuthClientCardProps) => {
console.log(areEmailsEnabled);
const router = useRouter();
const clientPermissions = Object.values(PERMISSIONS_GROUPED_MAP).map((value, index) => {
let permissionsMessage = "";
@@ -139,7 +140,14 @@ export const OAuthClientCard = ({
<span className="text-sm font-semibold">Emails enabled:</span> {areEmailsEnabled ? "Yes" : "No"}
</div>
</div>
<div className="flex items-start">
<div className="flex items-start gap-4">
<Button
className="bg-subtle hover:bg-emphasis text-white"
loading={isLoading}
disabled={isLoading}
onClick={() => router.push(`/settings/organizations/platform/oauth-clients/create?clientId=${id}`)}>
Edit
</Button>
<Button
className="bg-red-500 text-white hover:bg-red-600"
loading={isLoading}
@@ -1,6 +1,6 @@
import { useRouter } from "next/router";
import type { FC } from "react";
import React, { useState, useCallback } from "react";
import React, { useState, useCallback, useEffect } from "react";
import { useForm, useFieldArray } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -8,7 +8,24 @@ import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions"
import { showToast } from "@calcom/ui";
import { Meta, Button, TextField, Label, Tooltip } from "@calcom/ui";
import { useCreateOAuthClient } from "@lib/hooks/settings/organizations/platform/oauth-clients/usePersistOAuthClient";
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;
@@ -33,9 +50,10 @@ type FormValues = {
areEmailsEnabled?: boolean;
};
export const OAuthClientForm: FC = () => {
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<FormValues>({
defaultValues: {
redirectUris: [{ uri: "" }],
@@ -45,6 +63,32 @@ export const OAuthClientForm: FC = () => {
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(() => {
@@ -59,15 +103,27 @@ export const OAuthClientForm: FC = () => {
setIsSelectAllPermissionsChecked((preValue) => !preValue);
}, [isSelectAllPermissionsChecked, setValue]);
const { mutateAsync, isPending } = useCreateOAuthClient({
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;
@@ -82,17 +138,31 @@ export const OAuthClientForm: FC = () => {
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,
});
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;
@@ -109,6 +179,7 @@ export const OAuthClientForm: FC = () => {
id={`${permissionKey}Read`}
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={Boolean(clientId)}
/>
<label htmlFor={`${permissionKey}Read`} className="cursor-pointer text-sm">
Read
@@ -120,6 +191,7 @@ export const OAuthClientForm: FC = () => {
id={`${permissionKey}Write`}
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={Boolean(clientId)}
/>
<label htmlFor={`${permissionKey}Write`} className="cursor-pointer text-sm">
Write
@@ -133,15 +205,15 @@ export const OAuthClientForm: FC = () => {
return (
<div>
<Meta
title={t("oauth_form_title")}
description={t("oauth_form_description")}
title={t("oauth_form_title") + (clientId ? " - Update" : "")}
description={t("oauth_form_description") + (clientId ? " - Update" : "")}
borderInShellHeader={true}
/>
<form
className="border-subtle rounded-b-lg border border-t-0 px-4 pb-8 pt-2"
onSubmit={handleSubmit(onSubmit)}>
<div className="mt-6">
<TextField required={true} label="Client name" {...register("name")} />
<TextField disabled={disabledForm} required={true} label="Client name" {...register("name")} />
</div>
<div className="mt-6">
<Label>Redirect uris</Label>
@@ -154,6 +226,7 @@ export const OAuthClientForm: FC = () => {
required={index === 0}
className="w-[100%]"
label=""
disabled={disabledForm}
{...register(`redirectUris.${index}.uri` as const)}
/>
</div>
@@ -168,6 +241,7 @@ export const OAuthClientForm: FC = () => {
onClick={() => {
append({ uri: "" });
}}
disabled={disabledForm}
/>
{index > 0 && (
<Button
@@ -180,6 +254,7 @@ export const OAuthClientForm: FC = () => {
onClick={() => {
remove(index);
}}
disabled={disabledForm}
/>
)}
</div>
@@ -224,6 +299,7 @@ export const OAuthClientForm: FC = () => {
label="Booking redirect uri"
className="w-[100%]"
{...register("bookingRedirectUri")}
disabled={disabledForm}
/>
</Tooltip>
</div>
@@ -234,6 +310,7 @@ export const OAuthClientForm: FC = () => {
label="Booking cancel redirect uri"
className="w-[100%]"
{...register("bookingCancelRedirectUri")}
disabled={disabledForm}
/>
</Tooltip>
</div>
@@ -244,6 +321,7 @@ export const OAuthClientForm: FC = () => {
label="Booking reschedule redirect uri"
className="w-[100%]"
{...register("bookingRescheduleRedirectUri")}
disabled={disabledForm}
/>
</Tooltip>
</div>
@@ -253,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}
/>
<label htmlFor="areEmailsEnabled" className="cursor-pointer px-2 text-base font-semibold">
Enable emails
</label>
</div>
<div className="mt-6">
<div className="flex justify-between">
<h1 className="text-base font-semibold underline">Permissions</h1>
<Button type="button" onClick={selectAllPermissions}>
<Tooltip side="right" content="Permissions once set cannot be modified">
<h1 className="text-base font-semibold underline">Permissions</h1>
</Tooltip>
<Button type="button" onClick={selectAllPermissions} disabled={disabledForm || Boolean(clientId)}>
{!isSelectAllPermissionsChecked ? "Select all" : "Discard all"}
</Button>
</div>
<div>{permissionsCheckboxes}</div>
</div>
<Button className="mt-6" type="submit" loading={isPending}>
Submit
<Button className="mt-6" type="submit" loading={isPending} disabled={disabledForm}>
{clientId ? "Update" : "Submit"}
</Button>
</form>
</div>
@@ -17,20 +17,38 @@ export const useOAuthClients = () => {
return { ...query, data: query.data?.data ?? [] };
};
export const useOAuthClient = (clientId: string) => {
export const useOAuthClient = (clientId?: string) => {
const {
isLoading,
error,
data: response,
isFetched,
isError,
isFetching,
isSuccess,
isFetchedAfterMount,
refetch,
} = useQuery<ApiSuccessResponse<PlatformOAuthClient>>({
queryKey: ["oauth-client"],
queryKey: ["oauth-client", clientId],
queryFn: () => {
return fetch(`/api/v2/oauth-clients/${clientId}`, {
method: "get",
headers: { "Content-type": "application/json" },
}).then((res) => res.json());
},
enabled: clientId !== undefined,
staleTime: Infinity,
});
return { isLoading, error, data: response?.data };
return {
isLoading,
error,
data: response?.data,
isFetched,
isError,
isFetching,
isSuccess,
isFetchedAfterMount,
refetch,
};
};
@@ -46,6 +46,43 @@ export const useCreateOAuthClient = (
return mutation;
};
export const useUpdateOAuthClient = (
{ onSuccess, onError, clientId }: IPersistOAuthClient & { clientId?: string } = {
onSuccess: () => {
return;
},
onError: () => {
return;
},
}
) => {
const mutation = useMutation<
ApiResponse<{ clientId: string; clientSecret: string }>,
unknown,
Omit<CreateOAuthClientInput, "permissions">
>({
mutationFn: (data) => {
return fetch(`/api/v2/oauth-clients/${clientId}`, {
method: "PATCH",
headers: { "Content-type": "application/json" },
body: JSON.stringify(data),
}).then((res) => res?.json());
},
onSuccess: (data) => {
if (data.status === SUCCESS_STATUS) {
onSuccess?.();
} else {
onError?.();
}
},
onError: () => {
onError?.();
},
});
return mutation;
};
export const useDeleteOAuthClient = (
{ onSuccess, onError }: IPersistOAuthClient = {
onSuccess: () => {
@@ -62,7 +99,7 @@ export const useDeleteOAuthClient = (
return fetch(`/api/v2/oauth-clients/${id}`, {
method: "delete",
headers: { "Content-type": "application/json" },
}).then((res) => res.json());
}).then((res) => res?.json());
},
onSuccess: (data) => {
if (data.status === SUCCESS_STATUS) {
@@ -1,3 +1,4 @@
import { useRouter } from "next/router";
import React from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
@@ -6,9 +7,11 @@ import PageWrapper from "@components/PageWrapper";
import { OAuthClientForm } from "@components/settings/organizations/platform/oauth-clients/OAuthClientForm";
export const CreateOAuthClient = () => {
const router = useRouter();
const clientId = router.query.clientId as string | undefined;
return (
<div>
<OAuthClientForm />
<OAuthClientForm clientId={clientId} />
</div>
);
};
@@ -62,8 +62,6 @@ export const OAuthClients = () => {
<>
<div className="border-subtle rounded-b-lg border border-t-0">
{data.map((client, index) => {
console.log(client);
return (
<OAuthClientCard
name={client.name}
+20
View File
@@ -6,6 +6,10 @@ import {
PERMISSIONS,
SCHEDULE_READ,
SCHEDULE_WRITE,
APPS_READ,
APPS_WRITE,
PROFILE_READ,
PROFILE_WRITE,
} from "@calcom/platform-constants";
import type { PLATFORM_PERMISSION } from "@calcom/platform-types";
@@ -43,6 +47,22 @@ export const hasScheduleWritePermission = (userPermissions: number): boolean =>
return hasPermission(userPermissions, SCHEDULE_WRITE);
};
export const hasAppsReadPermission = (userPermissions: number): boolean => {
return hasPermission(userPermissions, APPS_READ);
};
export const hasAppsWritePermission = (userPermissions: number): boolean => {
return hasPermission(userPermissions, APPS_WRITE);
};
export const hasProfileReadPermission = (userPermissions: number): boolean => {
return hasPermission(userPermissions, PROFILE_READ);
};
export const hasProfileWritePermission = (userPermissions: number): boolean => {
return hasPermission(userPermissions, PROFILE_WRITE);
};
export const listPermissions = (userPermissions: number): PLATFORM_PERMISSION[] => {
return PERMISSIONS.reduce((acc, permission) => {
if (hasPermission(userPermissions, permission)) {