Files
calendar/apps/web/modules/settings/developer/oauth-clients-view.tsx
T
Eunjae LeeGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
81e4241f85 refactor: apply biome formatting to apps/web (#27692)
* refactor: apply biome formatting to apps/web (batch 1)

Formats apps/web non-modules directories:
- apps/web/app
- apps/web/lib
- apps/web/pages
- apps/web/styles
- apps/web/server
- apps/web/test
- Root-level .ts and .mjs files

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 2)

Formats smaller apps/web/modules directories:
- onboarding, data-table, users, apps, auth
- integration-attribute-sync, shell, availability
- feature-flags, team, webhooks, getting-started
- feature-opt-in, troubleshooter, schedules, notifications
- signup-view.tsx

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules (batch 3)

Formats medium apps/web/modules directories:
- bookings
- event-types
- insights
- embed

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/modules/ee (batch 4)

Formats apps/web/modules/ee directory.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web (batch 5)

Formats remaining apps/web directories:
- apps/web/modules/settings
- apps/web/modules/booking-audit
- apps/web/playwright

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to apps/web/components (batch 6)

Formats remaining apps/web/components files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Formats newly added/modified files from main merge:
- WorkflowStepContainer.tsx (resolved merge conflicts)
- Newly moved files (blocklist, form-builder, schedules, etc.)
- Other files modified in main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: apply biome formatting to new apps/web files from main

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 23:25:24 +05:30

193 lines
6.3 KiB
TypeScript

"use client";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { showToast } from "@calcom/ui/components/toast";
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
import type { OAuthClientCreateFormValues } from "../oauth/create/OAuthClientCreateModal";
import { OAuthClientCreateDialog } from "../oauth/create/OAuthClientCreateModal";
import { OAuthClientPreviewDialog } from "../oauth/create/OAuthClientPreviewDialog";
import { OAuthClientDetailsDialog, type OAuthClientDetails } from "../oauth/view/OAuthClientDetailsDialog";
import { OAuthClientsList } from "../oauth/OAuthClientsList";
import { NewOAuthClientButton } from "../oauth/create/NewOAuthClientButton";
import { OAuthClientsSkeleton } from "./oauth-clients-skeleton";
const OAuthClientsView = () => {
const { t } = useLocale();
const utils = trpc.useUtils();
const [isCreatingClient, setIsCreatingClient] = useState(false);
const [submittedClient, setSubmittedClient] = useState<OAuthClientDetails | null>(null);
const [selectedClient, setSelectedClient] = useState<OAuthClientDetails | null>(null);
const { data: oAuthClients, isLoading } = trpc.viewer.oAuth.listUserClients.useQuery();
const submitForReviewMutation = trpc.viewer.oAuth.submitClientForReview.useMutation({
onSuccess: async (data) => {
setSubmittedClient({
clientId: data.clientId,
name: data.name,
purpose: data.purpose ?? "",
clientSecret: data.clientSecret,
status: data.status,
isPkceEnabled: data.isPkceEnabled,
});
showToast(t("oauth_client_submitted"), "success");
utils.viewer.oAuth.listUserClients.invalidate();
},
onError: (error) => {
showToast(`${t("oauth_client_submit_error")}: ${error.message}`, "error");
},
});
const deleteClientMutation = trpc.viewer.oAuth.deleteClient.useMutation({
onSuccess: async () => {
showToast(t("oauth_client_deletion_message"), "success");
setSelectedClient(null);
utils.viewer.oAuth.listUserClients.invalidate();
},
onError: (error) => {
showToast(error.message || t("error"), "error");
},
});
const updateClientMutation = trpc.viewer.oAuth.updateClient.useMutation({
onSuccess: async (data) => {
showToast(t("oauth_client_updated_successfully"), "success");
setSelectedClient((prev) => {
if (!prev) return prev;
if (prev.clientId !== data.clientId) return prev;
return {
...prev,
name: data.name,
purpose: data.purpose,
redirectUri: data.redirectUri,
websiteUrl: data.websiteUrl,
logo: data.logo,
status: data.status,
rejectionReason: data.rejectionReason,
};
});
utils.viewer.oAuth.listUserClients.invalidate();
},
onError: (error) => {
showToast(`${t("updating_oauth_client_error")}: ${error.message}`, "error");
},
});
const handleSubmit = (values: OAuthClientCreateFormValues) => {
submitForReviewMutation.mutate({
name: values.name,
purpose: values.purpose,
redirectUri: values.redirectUri,
websiteUrl: values.websiteUrl,
logo: values.logo,
enablePkce: values.enablePkce,
});
};
const handleCloseCreateDialog = () => {
setIsCreatingClient(false);
setSubmittedClient(null);
};
const handleCloseDetailsDialog = () => {
setSelectedClient(null);
};
if (isLoading) {
return <OAuthClientsSkeleton />;
}
const newOAuthClientButton = (
<NewOAuthClientButton
dataTestId="open-oauth-client-create-dialog"
onClick={() => setIsCreatingClient(true)}
/>
);
return (
<SettingsHeader
title={t("oauth_clients")}
description={t("oauth_clients_description")}
CTA={newOAuthClientButton}
borderInShellHeader={true}>
<div>
{oAuthClients && oAuthClients.length > 0 ? (
<div className="border-subtle rounded-b-lg border border-t-0">
<OAuthClientsList
clients={oAuthClients.map((client) => ({
clientId: client.clientId,
name: client.name,
purpose: client.purpose,
redirectUri: client.redirectUri,
websiteUrl: client.websiteUrl,
logo: client.logo,
status: client.status,
rejectionReason: client.rejectionReason,
clientType: client.clientType,
}))}
onSelectClient={(client) => setSelectedClient(client)}
/>
</div>
) : (
<EmptyScreen
Icon="key"
headline={t("no_oauth_clients")}
description={t("no_oauth_clients_description")}
className="rounded-b-lg rounded-t-none border-t-0"
buttonRaw={newOAuthClientButton}
/>
)}
</div>
{submittedClient ? (
<OAuthClientPreviewDialog
open={isCreatingClient}
onOpenChange={setIsCreatingClient}
title={t("oauth_client_submitted")}
description={t("oauth_client_submitted_description")}
client={submittedClient}
onClose={handleCloseCreateDialog}
/>
) : (
<OAuthClientCreateDialog
open={isCreatingClient}
onOpenChange={setIsCreatingClient}
isSubmitting={submitForReviewMutation.isPending}
onSubmit={handleSubmit}
onClose={handleCloseCreateDialog}
/>
)}
<OAuthClientDetailsDialog
open={Boolean(selectedClient)}
onOpenChange={(open) => !open && handleCloseDetailsDialog()}
client={selectedClient}
onUpdate={(values) => {
updateClientMutation.mutate({
clientId: values.clientId,
name: values.name,
purpose: values.purpose,
redirectUri: values.redirectUri,
websiteUrl: values.websiteUrl,
logo: values.logo,
});
}}
onDelete={(clientId) => {
deleteClientMutation.mutate({ clientId });
}}
isUpdatePending={updateClientMutation.isPending}
isDeletePending={deleteClientMutation.isPending}
/>
</SettingsHeader>
);
};
export default OAuthClientsView;