feat: CalendarSettings atom (#16120)
* dumb components for calendar settings * shadcn switch * update exports * update packages * add calendar settings atom to examples app * init calendar settings atom * refactors * fix import path * export type for calendar switch props * invalidate queries on deleting calendar credentials * replace calendars list with calendar settings wrapper * cleanup * update styling * refactors * cleanup * fix: missing key prop CalendarSettingsPlatformWrapper * Label as client components * Address client component build errors * Move QueryCell out of packages/lib * PR feedback * more feedback --------- Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com> Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
This commit is contained in:
54 files changed
+781
-258
No files matched your search
@@ -1,15 +1,14 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { ReactNode } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui";
|
||||
import { AppListCard as AppListCardComponent } from "@calcom/ui";
|
||||
|
||||
type ShouldHighlight =
|
||||
| {
|
||||
@@ -21,7 +20,7 @@ type ShouldHighlight =
|
||||
slug?: never;
|
||||
};
|
||||
|
||||
type AppListCardProps = {
|
||||
export type AppListCardProps = {
|
||||
logo?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
@@ -37,21 +36,7 @@ type AppListCardProps = {
|
||||
const schema = z.object({ hl: z.string().optional() });
|
||||
|
||||
export default function AppListCard(props: AppListCardProps) {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
logo,
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
isDefault,
|
||||
slug,
|
||||
shouldHighlight,
|
||||
isTemplate,
|
||||
invalidCredential,
|
||||
children,
|
||||
credentialOwner,
|
||||
className,
|
||||
} = props;
|
||||
const { slug, shouldHighlight } = props;
|
||||
const {
|
||||
data: { hl },
|
||||
} = useTypedQuery(schema);
|
||||
@@ -83,53 +68,5 @@ export default function AppListCard(props: AppListCardProps) {
|
||||
};
|
||||
}, [highlight, pathname, router, searchParams, shouldHighlight]);
|
||||
|
||||
return (
|
||||
<div className={classNames(highlight && "dark:bg-muted bg-yellow-100", className)}>
|
||||
<div className="flex items-center gap-x-3 px-4 py-4 sm:px-6">
|
||||
{logo ? (
|
||||
<img
|
||||
className={classNames(logo.includes("-dark") && "dark:invert", "h-10 w-10")}
|
||||
src={logo}
|
||||
alt={`${title} logo`}
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex grow flex-col gap-y-1 truncate">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isDefault && <Badge variant="green">{t("default")}</Badge>}
|
||||
{isTemplate && <Badge variant="red">Template</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
<ListItemText component="p">{description}</ListItemText>
|
||||
{invalidCredential && (
|
||||
<div className="flex gap-x-2 pt-2">
|
||||
<Icon name="circle-alert" className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||
{t("invalid_credential")}
|
||||
</ListItemText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{credentialOwner && (
|
||||
<div>
|
||||
<Badge variant="gray">
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-2"
|
||||
alt={credentialOwner.name || "Nameless"}
|
||||
size="xs"
|
||||
imageSrc={getPlaceholderAvatar(credentialOwner.avatar, credentialOwner?.name as string)}
|
||||
/>
|
||||
{credentialOwner.name}
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{actions}
|
||||
</div>
|
||||
{children && <div className="w-full">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
return <AppListCardComponent {...props} />;
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import type { FunctionComponent, SVGProps } from "react";
|
||||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { QueryCell } from "@calcom/trpc/components/QueryCell";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
@@ -12,8 +13,6 @@ import {
|
||||
DropdownMenuTrigger,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
interface AdditionalCalendarSelectorProps {
|
||||
isPending?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
import Link from "next/link";
|
||||
import { Fragment, useEffect } from "react";
|
||||
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||
import { CalendarSettingsWebWrapper } from "@calcom/atoms/monorepo";
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
EmptyScreen,
|
||||
Label,
|
||||
@@ -22,7 +19,6 @@ import { QueryCell } from "@lib/QueryCell";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
import AppListCard from "@components/AppListCard";
|
||||
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
|
||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||
|
||||
type Props = {
|
||||
@@ -68,118 +64,6 @@ function CalendarList(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
// todo: @hariom extract this into packages/apps-store as "GeneralAppSettings"
|
||||
function ConnectedCalendarsList(props: Props) {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
|
||||
suspense: true,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const { fromOnboarding, isPending } = props;
|
||||
return (
|
||||
<QueryCell
|
||||
query={query}
|
||||
empty={() => null}
|
||||
success={({ data }) => {
|
||||
if (!data.connectedCalendars.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="border-subtle mt-6 rounded-lg border">
|
||||
<div className="border-subtle border-b p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-emphasis text-base font-semibold leading-5">
|
||||
{t("check_for_conflicts")}
|
||||
</h4>
|
||||
<p className="text-default text-sm leading-tight">{t("select_calendars")}</p>
|
||||
</div>
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
||||
{!!data.connectedCalendars.length && (
|
||||
<div className="flex items-center">
|
||||
<AdditionalCalendarSelector isPending={isPending} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<List noBorderTreatment className="p-6 pt-2">
|
||||
{data.connectedCalendars.map((item) => (
|
||||
<Fragment key={item.credentialId}>
|
||||
{item.calendars ? (
|
||||
<AppListCard
|
||||
shouldHighlight
|
||||
slug={item.integration.slug}
|
||||
title={item.integration.name}
|
||||
logo={item.integration.logo}
|
||||
description={item.primary?.email ?? item.integration.description}
|
||||
className="border-subtle mt-4 rounded-lg border"
|
||||
actions={
|
||||
<div className="flex w-32 justify-end">
|
||||
<DisconnectIntegration
|
||||
credentialId={item.credentialId}
|
||||
trashIcon
|
||||
onSuccess={props.onChanged}
|
||||
buttonProps={{ className: "border border-default" }}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<div className="border-subtle border-t">
|
||||
{!fromOnboarding && (
|
||||
<>
|
||||
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
|
||||
<ul className="space-y-4 px-5 py-4">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
externalId={cal.externalId}
|
||||
title={cal.name || "Nameless calendar"}
|
||||
name={cal.name || "Nameless calendar"}
|
||||
type={item.integration.type}
|
||||
isChecked={cal.isSelected}
|
||||
destination={cal.externalId === props.destinationCalendarId}
|
||||
credentialId={cal.credentialId}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AppListCard>
|
||||
) : (
|
||||
<Alert
|
||||
severity="warning"
|
||||
title={t("something_went_wrong")}
|
||||
message={
|
||||
<span>
|
||||
<Link href={`/apps/${item.integration.slug}`}>{item.integration.name}</Link>:{" "}
|
||||
{t("calendar_error")}
|
||||
</span>
|
||||
}
|
||||
iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5"
|
||||
actions={
|
||||
<div className="flex w-32 justify-end md:pr-1">
|
||||
<DisconnectIntegration
|
||||
credentialId={item.credentialId}
|
||||
trashIcon
|
||||
onSuccess={props.onChanged}
|
||||
buttonProps={{ className: "border border-default" }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function CalendarListContainer(props: { heading?: boolean; fromOnboarding?: boolean }) {
|
||||
const { t } = useLocale();
|
||||
const { heading = true, fromOnboarding } = props;
|
||||
@@ -245,7 +129,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ConnectedCalendarsList
|
||||
<CalendarSettingsWebWrapper
|
||||
onChanged={onChanged}
|
||||
fromOnboarding={fromOnboarding}
|
||||
destinationCalendarId={data.destinationCalendar?.externalId}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef } from "react";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useRef, useState, useCallback } from "react";
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { ButtonProps } from "@calcom/ui";
|
||||
import { Button, ConfirmationDialogContent, Dialog, DialogTrigger, showToast } from "@calcom/ui";
|
||||
import { DisconnectIntegrationComponent, showToast } from "@calcom/ui";
|
||||
|
||||
export default function DisconnectIntegration({
|
||||
credentialId,
|
||||
label,
|
||||
trashIcon,
|
||||
isGlobal,
|
||||
onSuccess,
|
||||
buttonProps,
|
||||
}: {
|
||||
export default function DisconnectIntegration(props: {
|
||||
credentialId: number;
|
||||
label?: string;
|
||||
trashIcon?: boolean;
|
||||
@@ -21,6 +16,7 @@ export default function DisconnectIntegration({
|
||||
buttonProps?: ButtonProps;
|
||||
}) {
|
||||
const { t } = useLocale();
|
||||
const { onSuccess, credentialId } = props;
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const utils = trpc.useUtils();
|
||||
|
||||
@@ -41,29 +37,11 @@ export default function DisconnectIntegration({
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
color={buttonProps?.color || "destructive"}
|
||||
StartIcon={!trashIcon ? undefined : "trash"}
|
||||
size="base"
|
||||
variant={trashIcon && !label ? "icon" : "button"}
|
||||
disabled={isGlobal}
|
||||
{...buttonProps}>
|
||||
{label && label}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
variety="danger"
|
||||
title={t("remove_app")}
|
||||
confirmBtnText={t("yes_remove_app")}
|
||||
onConfirm={() => {
|
||||
mutation.mutate({ id: credentialId });
|
||||
}}>
|
||||
<p className="mt-5">{t("are_you_sure_you_want_to_remove_this_app")}</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
<DisconnectIntegrationComponent
|
||||
onDeletionConfirmation={() => mutation.mutate({ id: credentialId })}
|
||||
isModalOpen={modalOpen}
|
||||
onModalOpen={() => setModalOpen((prevValue) => !prevValue)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { create } from "zustand";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -6,7 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon, showToast, Switch } from "@calcom/ui";
|
||||
|
||||
interface ICalendarSwitchProps {
|
||||
export type ICalendarSwitchProps = {
|
||||
title: string;
|
||||
externalId: string;
|
||||
type: string;
|
||||
@@ -15,7 +17,7 @@ interface ICalendarSwitchProps {
|
||||
isLastItemInList?: boolean;
|
||||
destination?: boolean;
|
||||
credentialId: number;
|
||||
}
|
||||
};
|
||||
const CalendarSwitch = (props: ICalendarSwitchProps) => {
|
||||
const { title, externalId, type, isChecked, name, isLastItemInList = false, credentialId } = props;
|
||||
const [checkedInternal, setCheckedInternal] = useState(isChecked);
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState, useEffect } from "react";
|
||||
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useEffect, type ReactNode } from "react";
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export const CalendarSettings = (props: { children: ReactNode }) => {
|
||||
return <div className="border-subtle mt-6 rounded-lg border">{props.children}</div>;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { CalendarSettingsPlatformWrapper } from "./wrappers/CalendarSettingsPlatformWrapper";
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import type { ICalendarSwitchProps } from "@calcom/features/calendars/CalendarSwitch";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { CALENDARS } from "@calcom/platform-constants";
|
||||
import { QueryCell } from "@calcom/trpc/components/QueryCell";
|
||||
import type { ButtonProps } from "@calcom/ui";
|
||||
import {
|
||||
CalendarSwitchComponent,
|
||||
AppListCard,
|
||||
List,
|
||||
DisconnectIntegrationComponent,
|
||||
Alert,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { useAddSelectedCalendar } from "../../hooks/calendars/useAddSelectedCalendar";
|
||||
import { useDeleteCalendarCredentials } from "../../hooks/calendars/useDeleteCalendarCredentials";
|
||||
import { useRemoveSelectedCalendar } from "../../hooks/calendars/useRemoveSelectedCalendar";
|
||||
import { useConnectedCalendars } from "../../hooks/useConnectedCalendars";
|
||||
import { AtomsWrapper } from "../../src/components/atoms-wrapper";
|
||||
import { Switch } from "../../src/components/ui/switch";
|
||||
import { useToast } from "../../src/components/ui/use-toast";
|
||||
import { CalendarSettings } from "../CalendarSettings";
|
||||
|
||||
export const CalendarSettingsPlatformWrapper = () => {
|
||||
const { t } = useLocale();
|
||||
const query = useConnectedCalendars({});
|
||||
|
||||
return (
|
||||
<AtomsWrapper>
|
||||
<div>
|
||||
<QueryCell
|
||||
query={query}
|
||||
success={({ data }) => {
|
||||
const destinationCalendarId = data.destinationCalendar.externalId;
|
||||
|
||||
if (!data.connectedCalendars.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CalendarSettings>
|
||||
<CalendarSettingsHeading />
|
||||
<List noBorderTreatment className="p-6 pt-2">
|
||||
{data.connectedCalendars.map((connectedCalendar) => {
|
||||
if (!!connectedCalendar.calendars && connectedCalendar.calendars.length > 0) {
|
||||
return (
|
||||
<AppListCard
|
||||
key={`list-${connectedCalendar.credentialId}-${connectedCalendar.integration.slug}`}
|
||||
shouldHighlight
|
||||
slug={connectedCalendar.integration.slug}
|
||||
title={connectedCalendar.integration.name}
|
||||
logo={`https://app.cal.com${connectedCalendar.integration.logo}`}
|
||||
description={
|
||||
connectedCalendar.primary?.email ?? connectedCalendar.integration.description
|
||||
}
|
||||
className="border-subtle mt-4 rounded-lg border"
|
||||
actions={
|
||||
<div className="flex w-32 justify-end">
|
||||
<PlatformDisconnectIntegration
|
||||
credentialId={connectedCalendar.credentialId}
|
||||
trashIcon
|
||||
buttonProps={{ className: "border border-default" }}
|
||||
slug={connectedCalendar.integration.slug}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<div className="border-subtle border-t">
|
||||
<p className="text-subtle px-5 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
|
||||
<ul className="space-y-4 px-5 py-4">
|
||||
{connectedCalendar.calendars?.map((cal) => {
|
||||
return (
|
||||
<PlatformCalendarSwitch
|
||||
key={cal.externalId}
|
||||
externalId={cal.externalId}
|
||||
title={cal.name || "Nameless calendar"}
|
||||
name={cal.name || "Nameless calendar"}
|
||||
type={connectedCalendar.integration.type}
|
||||
isChecked={cal.isSelected}
|
||||
destination={cal.externalId === destinationCalendarId}
|
||||
credentialId={cal.credentialId}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
</AppListCard>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert
|
||||
key={`alert-${connectedCalendar.credentialId}`}
|
||||
severity="warning"
|
||||
title={t("something_went_wrong")}
|
||||
message={<span>{t("calendar_error")}</span>}
|
||||
iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5"
|
||||
actions={
|
||||
<div className="flex w-32 justify-end">
|
||||
<PlatformDisconnectIntegration
|
||||
credentialId={connectedCalendar.credentialId}
|
||||
trashIcon
|
||||
buttonProps={{ className: "border border-default" }}
|
||||
slug={connectedCalendar.integration.slug}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</CalendarSettings>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</AtomsWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const CalendarSettingsHeading = () => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="border-subtle border-b p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-emphasis text-base font-semibold leading-5">{t("check_for_conflicts")}</h4>
|
||||
<p className="text-default text-sm leading-tight">{t("select_calendars")}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const PlatformDisconnectIntegration = (props: {
|
||||
credentialId: number;
|
||||
label?: string;
|
||||
slug?: string;
|
||||
trashIcon?: boolean;
|
||||
isGlobal?: boolean;
|
||||
onSuccess?: () => void;
|
||||
buttonProps?: ButtonProps;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const { onSuccess, credentialId, slug } = props;
|
||||
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
const { toast } = useToast();
|
||||
const { mutate: deleteCalendarCredentials } = useDeleteCalendarCredentials({
|
||||
onSuccess: () => {
|
||||
toast({
|
||||
description: t("app_removed_successfully"),
|
||||
});
|
||||
setModalOpen(false);
|
||||
onSuccess && onSuccess();
|
||||
},
|
||||
onError: () => {
|
||||
toast({
|
||||
description: t("error_removing_app"),
|
||||
});
|
||||
setModalOpen(false);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<DisconnectIntegrationComponent
|
||||
onDeletionConfirmation={async () => {
|
||||
slug &&
|
||||
(await deleteCalendarCredentials({
|
||||
calendar: slug.split("-")[0] as unknown as (typeof CALENDARS)[number],
|
||||
id: credentialId,
|
||||
}));
|
||||
}}
|
||||
{...props}
|
||||
isModalOpen={modalOpen}
|
||||
onModalOpen={() => setModalOpen((prevValue) => !prevValue)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const PlatformCalendarSwitch = (props: ICalendarSwitchProps) => {
|
||||
const { isChecked, title, credentialId, type, externalId } = props;
|
||||
const [checkedInternal, setCheckedInternal] = useState(isChecked);
|
||||
const { toast } = useToast();
|
||||
|
||||
const { mutate: addSelectedCalendar, isPending: isAddingSelectedCalendar } = useAddSelectedCalendar({
|
||||
onError: (err) => {
|
||||
toast({
|
||||
description: `Something went wrong while adding calendar - ${title}. ${err}`,
|
||||
});
|
||||
},
|
||||
});
|
||||
const { mutate: removeSelectedCalendar, isPending: isRemovingSelectedCalendar } = useRemoveSelectedCalendar(
|
||||
{
|
||||
onError: (err) => {
|
||||
toast({
|
||||
description: `Something went wrong while removing calendar - ${title}. ${err}`,
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
const toggleSelectedCalendars = async ({
|
||||
isOn,
|
||||
credentialId,
|
||||
integration,
|
||||
externalId,
|
||||
}: {
|
||||
isOn: boolean;
|
||||
credentialId: number;
|
||||
integration: string;
|
||||
externalId: string;
|
||||
}) => {
|
||||
if (isOn) {
|
||||
await addSelectedCalendar({ credentialId, integration, externalId });
|
||||
} else {
|
||||
await removeSelectedCalendar({ credentialId, integration, externalId });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<CalendarSwitchComponent
|
||||
destination={props.destination}
|
||||
{...props}
|
||||
isChecked={checkedInternal}
|
||||
isLoading={isAddingSelectedCalendar || isRemovingSelectedCalendar}>
|
||||
<Switch
|
||||
checked={checkedInternal}
|
||||
id={externalId}
|
||||
onCheckedChange={async () => {
|
||||
setCheckedInternal((prevValue) => !prevValue);
|
||||
await toggleSelectedCalendars({
|
||||
isOn: !checkedInternal,
|
||||
credentialId,
|
||||
externalId,
|
||||
integration: type,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</CalendarSwitchComponent>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,152 @@
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { QueryCell } from "@calcom/trpc/components/QueryCell";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert } from "@calcom/ui";
|
||||
import { List } from "@calcom/ui";
|
||||
import AppListCard from "@calcom/web/components/AppListCard";
|
||||
import AdditionalCalendarSelector from "@calcom/web/components/apps/AdditionalCalendarSelector";
|
||||
|
||||
import { CalendarSettings } from "../CalendarSettings";
|
||||
|
||||
type CalendarSettingsWebWrapperProps = {
|
||||
onChanged: () => unknown | Promise<unknown>;
|
||||
fromOnboarding?: boolean;
|
||||
destinationCalendarId?: string;
|
||||
isPending?: boolean;
|
||||
};
|
||||
|
||||
export const CalendarSettingsWebWrapper = (props: CalendarSettingsWebWrapperProps) => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
|
||||
suspense: true,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const { fromOnboarding, isPending } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<QueryCell
|
||||
query={query}
|
||||
success={({ data }) => {
|
||||
if (!data.connectedCalendars.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CalendarSettings>
|
||||
<CalendarSettingsHeading
|
||||
isConnectedCalendarsPresent={!!data.connectedCalendars.length}
|
||||
isPending={isPending}
|
||||
/>
|
||||
<List noBorderTreatment className="p-6 pt-2">
|
||||
{data.connectedCalendars.map((connectedCalendar) => {
|
||||
if (!!connectedCalendar.calendars && connectedCalendar.calendars.length > 0) {
|
||||
return (
|
||||
<AppListCard
|
||||
key={`list-${connectedCalendar.credentialId}`}
|
||||
shouldHighlight
|
||||
slug={connectedCalendar.integration.slug}
|
||||
title={connectedCalendar.integration.name}
|
||||
logo={connectedCalendar.integration.logo}
|
||||
description={
|
||||
connectedCalendar.primary?.email ?? connectedCalendar.integration.description
|
||||
}
|
||||
className="border-subtle mt-4 rounded-lg border"
|
||||
actions={
|
||||
<div className="flex w-32 justify-end">
|
||||
<DisconnectIntegration
|
||||
credentialId={connectedCalendar.credentialId}
|
||||
trashIcon
|
||||
onSuccess={props.onChanged}
|
||||
buttonProps={{ className: "border border-default" }}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<div className="border-subtle border-t">
|
||||
{!fromOnboarding && (
|
||||
<>
|
||||
<p className="text-subtle px-5 pt-4 text-sm">
|
||||
{t("toggle_calendars_conflict")}
|
||||
</p>
|
||||
<ul className="space-y-4 px-5 py-4">
|
||||
{connectedCalendar.calendars?.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
externalId={cal.externalId}
|
||||
title={cal.name || "Nameless calendar"}
|
||||
name={cal.name || "Nameless calendar"}
|
||||
type={connectedCalendar.integration.type}
|
||||
isChecked={cal.isSelected}
|
||||
destination={cal.externalId === props.destinationCalendarId}
|
||||
credentialId={cal.credentialId}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</AppListCard>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Alert
|
||||
key={`alert-${connectedCalendar.credentialId}`}
|
||||
severity="warning"
|
||||
title={t("something_went_wrong")}
|
||||
message={
|
||||
<span>
|
||||
<Link href={`/apps/${connectedCalendar.integration.slug}`}>
|
||||
{connectedCalendar.integration.name}
|
||||
</Link>
|
||||
: {t("calendar_error")}
|
||||
</span>
|
||||
}
|
||||
iconClassName="h-10 w-10 ml-2 mr-1 mt-0.5"
|
||||
actions={
|
||||
<div className="flex w-32 justify-end">
|
||||
<DisconnectIntegration
|
||||
credentialId={connectedCalendar.credentialId}
|
||||
trashIcon
|
||||
onSuccess={props.onChanged}
|
||||
buttonProps={{ className: "border border-default" }}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</List>
|
||||
</CalendarSettings>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CalendarSettingsHeading = (props: { isConnectedCalendarsPresent: boolean; isPending?: boolean }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="border-subtle border-b p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-emphasis text-base font-semibold leading-5">{t("check_for_conflicts")}</h4>
|
||||
<p className="text-default text-sm leading-tight">{t("select_calendars")}</p>
|
||||
</div>
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
||||
{props.isConnectedCalendarsPresent && (
|
||||
<div className="flex items-center">
|
||||
<AdditionalCalendarSelector isPending={props.isPending} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||
|
||||
import type { CALENDARS } from "@calcom/platform-constants";
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import type { ApiErrorResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
import http from "../../lib/http";
|
||||
import { QUERY_KEY } from "../useConnectedCalendars";
|
||||
|
||||
interface IUseDeleteCalendarCredentials {
|
||||
onSuccess?: (res: ApiResponse) => void;
|
||||
@@ -21,6 +22,7 @@ export const useDeleteCalendarCredentials = (
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const queryClient = useQueryClient();
|
||||
const deleteCalendarCredentials = useMutation<
|
||||
ApiResponse<{
|
||||
status: string;
|
||||
@@ -56,6 +58,9 @@ export const useDeleteCalendarCredentials = (
|
||||
onError: (err) => {
|
||||
onError?.(err as ApiErrorResponse);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({ queryKey: [QUERY_KEY] });
|
||||
},
|
||||
});
|
||||
|
||||
return deleteCalendarCredentials;
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
import type { translationKeys, CalProviderLanguagesType } from "../cal-provider/CalProvider";
|
||||
|
||||
@@ -20,3 +20,4 @@ export { useDeleteCalendarCredentials } from "./hooks/calendars/useDeleteCalenda
|
||||
export { useAddSelectedCalendar } from "./hooks/calendars/useAddSelectedCalendar";
|
||||
export { useRemoveSelectedCalendar } from "./hooks/calendars/useRemoveSelectedCalendar";
|
||||
export { useTeams } from "./hooks/teams/useTeams";
|
||||
export { CalendarSettingsPlatformWrapper as CalendarSettings } from "./calendar-settings/wrappers/CalendarSettingsPlatformWrapper";
|
||||
@@ -7,3 +7,4 @@ export { useEventTypeById } from "./hooks/event-types/private/useEventTypeById";
|
||||
export { useHandleBookEvent } from "./hooks/useHandleBookEvent";
|
||||
export * as Dialog from "./src/components/ui/dialog";
|
||||
export { Timezone } from "./timezone";
|
||||
export { CalendarSettingsWebWrapper } from "./calendar-settings/wrappers/CalendarSettingsWebWrapper";
|
||||
@@ -57,6 +57,7 @@
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.0.4",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-switch": "^1.1.0",
|
||||
"@radix-ui/react-toast": "^1.1.5",
|
||||
"@tanstack/react-query": "^5.17.15",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { cn } from "@/lib/utils";
|
||||
import * as SwitchPrimitives from "@radix-ui/react-switch";
|
||||
import * as React from "react";
|
||||
|
||||
const Switch = React.forwardRef<
|
||||
React.ElementRef<typeof SwitchPrimitives.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<SwitchPrimitives.Root
|
||||
className={cn(
|
||||
"focus-visible:ring-ring focus-visible:ring-offset-background data-[state=checked]:bg-primary data-[state=unchecked]:bg-input peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
ref={ref}>
|
||||
<SwitchPrimitives.Thumb
|
||||
className={cn(
|
||||
"bg-background pointer-events-none block h-5 w-5 rounded-full shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
|
||||
)}
|
||||
/>
|
||||
</SwitchPrimitives.Root>
|
||||
));
|
||||
Switch.displayName = SwitchPrimitives.Root.displayName;
|
||||
|
||||
export { Switch };
|
||||
@@ -1,47 +1,24 @@
|
||||
import { Navbar } from "@/components/Navbar";
|
||||
import { Inter } from "next/font/google";
|
||||
|
||||
import { useConnectedCalendars } from "@calcom/atoms";
|
||||
import { useConnectedCalendars, CalendarSettings } from "@calcom/atoms";
|
||||
|
||||
const inter = Inter({ subsets: ["latin"] });
|
||||
|
||||
export default function Calendars(props: { calUsername: string; calEmail: string }) {
|
||||
const { isLoading, data: calendars } = useConnectedCalendars();
|
||||
const { isLoading, data: calendars } = useConnectedCalendars({});
|
||||
const connectedCalendars = calendars?.connectedCalendars ?? [];
|
||||
const destinationCalendar = calendars?.destinationCalendar ?? {};
|
||||
|
||||
return (
|
||||
<main className={`flex min-h-screen flex-col ${inter.className}`}>
|
||||
<Navbar username={props.calUsername} />
|
||||
<div className="p-4">
|
||||
{!!connectedCalendars?.length ? (
|
||||
<h1 className="my-4 text-lg font-bold">Your Connected Calendars</h1>
|
||||
) : (
|
||||
<h1 className="mx-10 my-4 text-xl font-bold">
|
||||
You have not connected any calendars yet, please connect your Google calendar.
|
||||
</h1>
|
||||
)}
|
||||
{isLoading ? (
|
||||
<div>Loading...</div>
|
||||
) : (
|
||||
Boolean(connectedCalendars?.length) &&
|
||||
connectedCalendars.map((connectedCalendar) => (
|
||||
<div key={connectedCalendar.credentialId}>
|
||||
<h1 className="text-md font-bold">{connectedCalendar.integration.name}</h1>
|
||||
{connectedCalendar.calendars?.map((calendar) => (
|
||||
<div key={calendar.id}>
|
||||
<h2>{calendar.name}</h2>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
{!!connectedCalendars?.length && <hr className="my-4" />}
|
||||
{!isLoading && destinationCalendar.id && (
|
||||
<div className="">
|
||||
<h2 className="text-md font-bold">Destination Calendar: {destinationCalendar.name}</h2>
|
||||
<p>{destinationCalendar.integrationTitle}</p>
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && !connectedCalendars?.length && (
|
||||
<h1 className="mx-10 my-4 text-xl font-bold">
|
||||
You have not connected any calendars yet, please connect your Google, Outlook or Apple calendar.
|
||||
</h1>
|
||||
)}
|
||||
<div className="mx-10 my-4">
|
||||
<CalendarSettings />
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import type {
|
||||
QueryObserverPendingResult,
|
||||
QueryObserverRefetchErrorResult,
|
||||
QueryObserverSuccessResult,
|
||||
QueryObserverLoadingErrorResult,
|
||||
UseQueryResult,
|
||||
} from "@tanstack/react-query";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Alert, Loader } from "@calcom/ui";
|
||||
|
||||
type ErrorLike = {
|
||||
message: string;
|
||||
};
|
||||
type JSXElementOrNull = JSX.Element | null;
|
||||
|
||||
interface QueryCellOptionsBase<TData, TError extends ErrorLike> {
|
||||
query: UseQueryResult<TData, TError>;
|
||||
customLoader?: ReactNode;
|
||||
error?: (
|
||||
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
|
||||
) => JSXElementOrNull;
|
||||
loading?: (query: QueryObserverPendingResult<TData, TError> | null) => JSXElementOrNull;
|
||||
}
|
||||
|
||||
interface QueryCellOptionsNoEmpty<TData, TError extends ErrorLike>
|
||||
extends QueryCellOptionsBase<TData, TError> {
|
||||
success: (query: QueryObserverSuccessResult<TData, TError>) => JSXElementOrNull;
|
||||
}
|
||||
|
||||
interface QueryCellOptionsWithEmpty<TData, TError extends ErrorLike>
|
||||
extends QueryCellOptionsBase<TData, TError> {
|
||||
success: (query: QueryObserverSuccessResult<NonNullable<TData>, TError>) => JSXElementOrNull;
|
||||
/**
|
||||
* If there's no data (`null`, `undefined`, or `[]`), render this component
|
||||
*/
|
||||
empty: (query: QueryObserverSuccessResult<TData, TError>) => JSXElementOrNull;
|
||||
}
|
||||
|
||||
export function QueryCell<TData, TError extends ErrorLike>(
|
||||
opts: QueryCellOptionsWithEmpty<TData, TError>
|
||||
): JSXElementOrNull;
|
||||
export function QueryCell<TData, TError extends ErrorLike>(
|
||||
opts: QueryCellOptionsNoEmpty<TData, TError>
|
||||
): JSXElementOrNull;
|
||||
/** @deprecated Use `trpc.useQuery` instead. */
|
||||
export function QueryCell<TData, TError extends ErrorLike>(
|
||||
opts: QueryCellOptionsNoEmpty<TData, TError> | QueryCellOptionsWithEmpty<TData, TError>
|
||||
) {
|
||||
const { query } = opts;
|
||||
const { isLocaleReady } = useLocale();
|
||||
const StatusLoader = opts.customLoader || <Loader />; // Fixes edge case where this can return null form query cell
|
||||
|
||||
if (!isLocaleReady) {
|
||||
return opts.loading?.(query.status === "pending" ? query : null) ?? StatusLoader;
|
||||
}
|
||||
if (query.status === "pending") {
|
||||
return opts.loading?.(query) ?? StatusLoader;
|
||||
}
|
||||
|
||||
if (query.status === "success") {
|
||||
if ("empty" in opts && (query.data == null || (Array.isArray(query.data) && query.data.length === 0))) {
|
||||
return opts.empty(query);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
return opts.success(query as any);
|
||||
}
|
||||
|
||||
if (query.status === "error") {
|
||||
return (
|
||||
opts.error?.(query) ?? (
|
||||
<Alert severity="error" title="Something went wrong" message={query.error.message} />
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// impossible state
|
||||
return null;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Avatar, Badge, Icon, ListItemText } from "@calcom/ui";
|
||||
|
||||
import type { AppListCardProps } from "../../../../apps/web/components/AppListCard";
|
||||
|
||||
export const AppListCard = (props: AppListCardProps & { highlight?: boolean }) => {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
logo,
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
isDefault,
|
||||
isTemplate,
|
||||
invalidCredential,
|
||||
children,
|
||||
credentialOwner,
|
||||
className,
|
||||
highlight,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={classNames(highlight && "dark:bg-muted bg-yellow-100", className)}>
|
||||
<div className="flex items-center gap-x-3 px-4 py-4 sm:px-6">
|
||||
{logo ? (
|
||||
<img
|
||||
className={classNames(logo.includes("-dark") && "dark:invert", "h-10 w-10")}
|
||||
src={logo}
|
||||
alt={`${title} logo`}
|
||||
/>
|
||||
) : null}
|
||||
<div className="flex grow flex-col gap-y-1 truncate">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isDefault && <Badge variant="green">{t("default")}</Badge>}
|
||||
{isTemplate && <Badge variant="red">Template</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
<ListItemText component="p">{description}</ListItemText>
|
||||
{invalidCredential && (
|
||||
<div className="flex gap-x-2 pt-2">
|
||||
<Icon name="circle-alert" className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||
{t("invalid_credential")}
|
||||
</ListItemText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{credentialOwner && (
|
||||
<div>
|
||||
<Badge variant="gray">
|
||||
<div className="flex items-center">
|
||||
<Avatar
|
||||
className="mr-2"
|
||||
alt={credentialOwner.name || "Nameless"}
|
||||
size="xs"
|
||||
imageSrc={getPlaceholderAvatar(credentialOwner.avatar, credentialOwner?.name as string)}
|
||||
/>
|
||||
{credentialOwner.name}
|
||||
</div>
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
{actions}
|
||||
</div>
|
||||
<div className="w-full">{children}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { AppListCard } from "./AppListCard";
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import type { AppCategories } from "@prisma/client";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { Options } from "@glidejs/glide";
|
||||
import Glide from "@glidejs/glide";
|
||||
import "@glidejs/glide/dist/css/glide.core.min.css";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Children, Fragment, useEffect, useState } from "react";
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { type ICalendarSwitchProps } from "@calcom/features/calendars/CalendarSwitch";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
export function CalendarSwitchComponent(
|
||||
props: ICalendarSwitchProps & {
|
||||
isLoading: boolean;
|
||||
children: ReactNode;
|
||||
translations?: {
|
||||
spanText?: string;
|
||||
};
|
||||
}
|
||||
) {
|
||||
const {
|
||||
externalId,
|
||||
name,
|
||||
isLoading,
|
||||
translations = {
|
||||
spanText: "Adding events to",
|
||||
},
|
||||
children,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div className={classNames("my-2 flex flex-row items-center")}>
|
||||
<div className="flex pl-2">{children}</div>
|
||||
<label className="ml-3 text-sm font-medium leading-5" htmlFor={externalId}>
|
||||
{name}
|
||||
</label>
|
||||
{!!props.destination && (
|
||||
<span className="bg-subtle text-default ml-8 inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm font-normal sm:ml-4">
|
||||
<Icon name="arrow-left" className="h-4 w-4" />
|
||||
{translations.spanText}
|
||||
</span>
|
||||
)}
|
||||
{isLoading && <Icon name="rotate-cw" className="text-muted h-4 w-4 animate-spin ltr:ml-1 rtl:mr-1" />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { CalendarSwitchComponent } from "./CalendarSwitch";
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import type { ForwardRefExoticComponent, ReactElement, ReactNode } from "react";
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { ButtonProps } from "@calcom/ui";
|
||||
import { Button, ConfirmationDialogContent, Dialog, DialogTrigger } from "@calcom/ui";
|
||||
|
||||
export const DisconnectIntegrationComponent = ({
|
||||
label,
|
||||
trashIcon,
|
||||
isGlobal,
|
||||
isModalOpen = false,
|
||||
onModalOpen,
|
||||
onDeletionConfirmation,
|
||||
buttonProps,
|
||||
}: {
|
||||
label?: string;
|
||||
trashIcon?: boolean;
|
||||
isGlobal?: boolean;
|
||||
isModalOpen: boolean;
|
||||
onModalOpen: () => void;
|
||||
onDeletionConfirmation: () => void;
|
||||
buttonProps?: ButtonProps;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={isModalOpen} onOpenChange={onModalOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
color={buttonProps?.color || "destructive"}
|
||||
StartIcon={!trashIcon ? undefined : "trash"}
|
||||
size="base"
|
||||
variant={trashIcon && !label ? "icon" : "button"}
|
||||
disabled={isGlobal}
|
||||
{...buttonProps}>
|
||||
{label}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
variety="danger"
|
||||
title={t("remove_app")}
|
||||
confirmBtnText={t("yes_remove_app")}
|
||||
onConfirm={onDeletionConfirmation}>
|
||||
<p className="mt-5">{t("are_you_sure_you_want_to_remove_this_app")}</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { DisconnectIntegrationComponent } from "./DisconnectIntegration";
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import classNames from "classnames";
|
||||
import { useState } from "react";
|
||||
import type { ControllerRenderProps } from "react-hook-form";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
||||
import {
|
||||
LexicalTypeaheadMenuPlugin,
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { $generateHtmlFromNodes, $generateNodesFromDOM } from "@lexical/html";
|
||||
import { $isLinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
||||
import {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { useState } from "react";
|
||||
import { HexColorInput, HexColorPicker } from "react-colorful";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
import React, { forwardRef, useCallback, useId, useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import React, { forwardRef, useId, useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import type { ITimezoneOption, ITimezone, Props as SelectProps } from "react-timezone-select";
|
||||
import BaseSelect from "react-timezone-select";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useId } from "@radix-ui/react-id";
|
||||
import { Root as ToggleGroupPrimitive, Item as ToggleGroupItemPrimitive } from "@radix-ui/react-toggle-group";
|
||||
import { useState } from "react";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState, useEffect } from "react";
|
||||
import Cropper from "react-easy-crop";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useState } from "react";
|
||||
import Cropper from "react-easy-crop";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import Head from "next/head";
|
||||
import { createContext, useContext, useState, useEffect } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { useState, type PropsWithChildren } from "react";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import type { PropsWithChildren } from "react";
|
||||
import React, { useRef, useEffect, useState } from "react";
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { isSupportedCountry } from "libphonenumber-js";
|
||||
import { useState, useEffect } from "react";
|
||||
import PhoneInput from "react-phone-input-2";
|
||||
|
||||
@@ -140,6 +140,9 @@ export {
|
||||
ConfirmationContent,
|
||||
} from "./components/dialog";
|
||||
export type { DialogProps, ConfirmationDialogContentProps } from "./components/dialog";
|
||||
export { AppListCard } from "./components/app-list-card";
|
||||
export { DisconnectIntegrationComponent } from "./components/disconnect-calendar-integration";
|
||||
export { CalendarSwitchComponent } from "./components/calendar-switch";
|
||||
export { showToast } from "./components/toast"; // We don't export the toast components as they are only used in local storybook file
|
||||
export { Meta, MetaProvider, useMeta } from "./components/meta";
|
||||
export { ShellSubHeading } from "./components/layout";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { noop } from "lodash";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
type CssVariables = Record<string, string>;
|
||||
|
||||
Reference in new issue
Block a user