Files
calendar/apps/web/components/apps/MultiDisconnectIntegration.tsx
T
Benny JooGitHubDevin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
00e5770273 refactor: Move attribute and routing-forms code from app-store/lib to features (#26025)
* mv

* wip

* wip

* wip

* wip

* plural

* fix: resolve TypeScript type errors for attribute refactoring

- Add TransformedAttributeOption type to handle transformed contains field
- Update imports to use routing-forms Attribute type where needed
- Fix getValueOfAttributeOption to use TransformedAttributeOption type
- Update AttributeOptionValueWithType to use TransformedAttributeOption
- Fix pre-existing lint warnings (any -> unknown, proper type casting)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

* cleanup

* cleanup

* fix

* fix

* fix

* fix

* fix

* fix: add explicit type annotation to reduce callback in getAttributes.ts

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: use RouterOutputs from @calcom/trpc/react for consistent type inference

- Update AppPage.tsx to use RouterOutputs from @calcom/trpc/react instead of inferRouterOutputs<AppRouter>
- Update MultiDisconnectIntegration.tsx to use the same RouterOutputs type source
- Add eslint-disable comments for pre-existing warnings (useEffect deps, img elements)

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: explicitly type attribute property in AttributeOptionAssignment

The attribute property from Pick<AttributeOption, 'attribute'> was typed as
'unknown' because Prisma relations don't have explicit types when picked.
This explicitly defines the attribute shape with id, type, and isLocked
properties that are used in assignValueToUser.ts.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: remove unused assignedUsers from AttributeOptionAssignment Pick

The assignedUsers property was not being used in the code but was required
by the Pick type, causing a type mismatch when the actual data from the
database query didn't include it.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix: update import path for AttributeOptionAssignment and BulkAttributeAssigner types

The types.d.ts file was moved from packages/lib/service/attribute/ to
packages/app-store/routing-forms/types/. Updated the import path in utils.ts
to point to the new location.

Co-Authored-By: benny@cal.com <sldisek783@gmail.com>

* fix

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2025-12-29 21:51:06 -03:00

115 lines
4.0 KiB
TypeScript

import { useState } from "react";
import { Dialog } from "@calcom/features/components/controlled-dialog";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc, type RouterOutputs } from "@calcom/trpc/react";
import { Button } from "@calcom/ui/components/button";
import { ConfirmationDialogContent } from "@calcom/ui/components/dialog";
import {
Dropdown,
DropdownItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@calcom/ui/components/dropdown";
import { showToast } from "@calcom/ui/components/toast";
type Credentials = RouterOutputs["viewer"]["apps"]["appCredentialsByType"]["credentials"];
interface Props {
credentials: Credentials;
onSuccess?: () => void;
}
export function MultiDisconnectIntegration({ credentials, onSuccess }: Props) {
const { t } = useLocale();
const utils = trpc.useUtils();
const [credentialToDelete, setCredentialToDelete] = useState<{
id: number;
teamId: number | null;
name: string | null;
} | null>(null);
const [confirmationDialogOpen, setConfirmationDialogOpen] = useState(false);
const mutation = trpc.viewer.credentials.delete.useMutation({
onSuccess: () => {
showToast(t("app_removed_successfully"), "success");
onSuccess?.();
setConfirmationDialogOpen(false);
},
onError: () => {
showToast(t("error_removing_app"), "error");
setConfirmationDialogOpen(false);
},
async onSettled() {
await utils.viewer.calendars.connectedCalendars.invalidate();
await utils.viewer.apps.integrations.invalidate();
},
});
const getUserDisplayName = (user: (typeof credentials)[number]["user"]): string | null => {
if (!user) return null;
// Check if 'name' property exists and has a truthy string value
if ("name" in user && typeof user.name === "string" && user.name) return user.name;
// Otherwise use email if available and it's a string
if ("email" in user && typeof user.email === "string" && user.email) return user.email;
return null;
};
return (
<>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button color="secondary">{t("disconnect")}</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuLabel>
<div className="w-48 text-left text-xs">{t("disconnect_app_from")}</div>
</DropdownMenuLabel>
{credentials.map((cred) => (
<DropdownMenuItem key={cred.id}>
<DropdownItem
type="button"
color="destructive"
className="hover:bg-subtle hover:text-emphasis w-full border-0"
StartIcon={cred.teamId ? "users" : "user"}
onClick={() => {
setCredentialToDelete({
id: cred.id,
teamId: cred.teamId,
name: cred.team?.name || getUserDisplayName(cred.user) || null,
});
setConfirmationDialogOpen(true);
}}>
<div className="flex flex-col text-left">
<span>{cred.team?.name || getUserDisplayName(cred.user) || t("unnamed")}</span>
</div>
</DropdownItem>
</DropdownMenuItem>
))}
</DropdownMenuContent>
</Dropdown>
<Dialog open={confirmationDialogOpen} onOpenChange={setConfirmationDialogOpen}>
<ConfirmationDialogContent
variety="danger"
title={t("remove_app")}
confirmBtnText={t("yes_remove_app")}
onConfirm={() => {
if (credentialToDelete) {
mutation.mutate({
id: credentialToDelete.id,
...(credentialToDelete.teamId ? { teamId: credentialToDelete.teamId } : {}),
});
}
}}>
<p className="mt-5">
{t("are_you_sure_you_want_to_remove_this_app_from")} {credentialToDelete?.name || t("unnamed")}?
</p>
</ConfirmationDialogContent>
</Dialog>
</>
);
}