Skip to content

Commit

Permalink
feat: CalendarSettings atom (#16120)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
5 people authored Aug 13, 2024
1 parent b09b32b commit 0b03bcb
Show file tree
Hide file tree
Showing 54 changed files with 781 additions and 258 deletions.
75 changes: 6 additions & 69 deletions apps/web/components/AppListCard.tsx
Original file line number Diff line number Diff line change
@@ -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 =
| {
Expand All @@ -21,7 +20,7 @@ type ShouldHighlight =
slug?: never;
};

type AppListCardProps = {
export type AppListCardProps = {
logo?: string;
title: string;
description: string;
Expand All @@ -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);
Expand Down Expand Up @@ -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} />;
}
3 changes: 1 addition & 2 deletions apps/web/components/apps/AdditionalCalendarSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -12,8 +13,6 @@ import {
DropdownMenuTrigger,
} from "@calcom/ui";

import { QueryCell } from "@lib/QueryCell";

interface AdditionalCalendarSelectorProps {
isPending?: boolean;
}
Expand Down
120 changes: 2 additions & 118 deletions apps/web/components/apps/CalendarListContainer.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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 = {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -245,7 +129,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
</div>
</div>
</div>
<ConnectedCalendarsList
<CalendarSettingsWebWrapper
onChanged={onChanged}
fromOnboarding={fromOnboarding}
destinationCalendarId={data.destinationCalendar?.externalId}
Expand Down
2 changes: 2 additions & 0 deletions packages/app-store/components.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import Link from "next/link";
import { useRouter } from "next/navigation";
import { useEffect, useRef } from "react";
Expand Down
2 changes: 2 additions & 0 deletions packages/embeds/embed-core/src/embed-iframe.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useRouter } from "next/navigation";
import { useEffect, useRef, useState, useCallback } from "react";

Expand Down
44 changes: 11 additions & 33 deletions packages/features/apps/components/DisconnectIntegration.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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();

Expand All @@ -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}
/>
);
}
2 changes: 2 additions & 0 deletions packages/features/bookings/Booker/store.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useEffect } from "react";
import { create } from "zustand";

Expand Down
6 changes: 4 additions & 2 deletions packages/features/calendars/CalendarSwitch.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useMutation } from "@tanstack/react-query";
import { useState } from "react";

Expand All @@ -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;
Expand All @@ -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);
Expand Down
2 changes: 2 additions & 0 deletions packages/platform/atoms/availability/AvailabilitySettings.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useMemo, useState, useEffect } from "react";
import { Controller, useFieldArray, useForm, useWatch } from "react-hook-form";

Expand Down
2 changes: 2 additions & 0 deletions packages/platform/atoms/booker/BookerWebWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
"use client";

import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
Expand Down
Loading

0 comments on commit 0b03bcb

Please sign in to comment.