chore: Refactor `<CalendarListContainer />` (#12388)
* Import calendarlistcontainer in settings * Change CalendarListComponent to Figma design * Add border subtle * Address feedback
This commit is contained in:
parent
bd6ca21e02
commit
57e6971942
|
@ -32,6 +32,7 @@ type AppListCardProps = {
|
|||
invalidCredential?: boolean;
|
||||
children?: ReactNode;
|
||||
credentialOwner?: CredentialOwner;
|
||||
className?: string;
|
||||
} & ShouldHighlight;
|
||||
|
||||
const schema = z.object({ hl: z.string().optional() });
|
||||
|
@ -50,6 +51,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||
invalidCredential,
|
||||
children,
|
||||
credentialOwner,
|
||||
className,
|
||||
} = props;
|
||||
const {
|
||||
data: { hl },
|
||||
|
@ -83,7 +85,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||
}, [highlight, pathname, router, searchParams, shouldHighlight]);
|
||||
|
||||
return (
|
||||
<div className={classNames(highlight && "dark:bg-muted bg-yellow-100")}>
|
||||
<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
|
||||
|
|
|
@ -14,8 +14,9 @@ import {
|
|||
List,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
ShellSubHeading,
|
||||
Label,
|
||||
} from "@calcom/ui";
|
||||
import { Calendar, Plus } from "@calcom/ui/components/icon";
|
||||
import { Calendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
|
@ -27,6 +28,7 @@ type Props = {
|
|||
onChanged: () => unknown | Promise<unknown>;
|
||||
fromOnboarding?: boolean;
|
||||
destinationCalendarId?: string;
|
||||
isLoading?: boolean;
|
||||
};
|
||||
|
||||
function CalendarList(props: Props) {
|
||||
|
@ -70,8 +72,9 @@ function ConnectedCalendarsList(props: Props) {
|
|||
const { t } = useLocale();
|
||||
const query = trpc.viewer.connectedCalendars.useQuery(undefined, {
|
||||
suspense: true,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const { fromOnboarding } = props;
|
||||
const { fromOnboarding, isLoading } = props;
|
||||
return (
|
||||
<QueryCell
|
||||
query={query}
|
||||
|
@ -82,74 +85,94 @@ function ConnectedCalendarsList(props: Props) {
|
|||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
{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}
|
||||
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 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 isLoading={isLoading} />
|
||||
</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>
|
||||
</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>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
</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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
@ -187,42 +210,28 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
||||
<>
|
||||
{heading && (
|
||||
<div className="border-default flex flex-col gap-6 rounded-md border p-7">
|
||||
<ShellSubHeading
|
||||
title={t("calendar")}
|
||||
subtitle={t("installed_app_calendar_description")}
|
||||
className="mb-0 flex flex-wrap items-center gap-4 sm:flex-nowrap md:mb-3 md:gap-0"
|
||||
actions={
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
||||
{!!data.connectedCalendars.length && (
|
||||
<div className="flex items-center">
|
||||
<AdditionalCalendarSelector isLoading={mutation.isLoading} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
<div className="bg-muted border-subtle flex justify-between rounded-md border p-4">
|
||||
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
||||
<div className="bg-default border-subtle relative rounded-md border p-1.5">
|
||||
<Calendar className="text-default h-8 w-8" strokeWidth="1" />
|
||||
<Plus
|
||||
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
</div>
|
||||
<div className="md:w-6/12">
|
||||
<h1 className="text-emphasis text-sm font-semibold">{t("create_events_on")}</h1>
|
||||
<p className="text-default text-sm font-normal">{t("set_calendar")}</p>
|
||||
</div>
|
||||
<div className="justify-end md:w-6/12">
|
||||
<DestinationCalendarSelector
|
||||
onChange={mutation.mutate}
|
||||
hidePlaceholder
|
||||
isLoading={mutation.isLoading}
|
||||
value={data.destinationCalendar?.externalId}
|
||||
hideAdvancedText
|
||||
/>
|
||||
<>
|
||||
<div className="border-subtle mb-6 mt-8 rounded-lg border">
|
||||
<div className="p-6">
|
||||
<h2 className="text-emphasis mb-1 text-base font-bold leading-5 tracking-wide">
|
||||
{t("add_to_calendar")}
|
||||
</h2>
|
||||
|
||||
<p className="text-subtle text-sm leading-tight">
|
||||
{t("add_to_calendar_description")}
|
||||
</p>
|
||||
</div>
|
||||
<div className="border-t">
|
||||
<div className="border-subtle flex w-full flex-col space-y-3 border-y-0 p-6">
|
||||
<div>
|
||||
<Label className="text-default mb-0 font-medium">{t("add_events_to")}</Label>
|
||||
<DestinationCalendarSelector
|
||||
hidePlaceholder
|
||||
value={data.destinationCalendar?.externalId}
|
||||
onChange={mutation.mutate}
|
||||
isLoading={mutation.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -230,8 +239,9 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||
onChanged={onChanged}
|
||||
fromOnboarding={fromOnboarding}
|
||||
destinationCalendarId={data.destinationCalendar?.externalId}
|
||||
isLoading={mutation.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : fromOnboarding ? (
|
||||
|
|
|
@ -1,37 +1,12 @@
|
|||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Fragment, useState, useEffect } from "react";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
import { CalendarSwitch } from "@calcom/features/calendars/CalendarSwitch";
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import SectionBottomActions from "@calcom/features/settings/SectionBottomActions";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Badge,
|
||||
Button,
|
||||
EmptyScreen,
|
||||
List,
|
||||
ListItem,
|
||||
ListItemText,
|
||||
ListItemTitle,
|
||||
Meta,
|
||||
SkeletonButton,
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
showToast,
|
||||
Label,
|
||||
} from "@calcom/ui";
|
||||
import { Plus, Calendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import { Button, Meta, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
|
@ -62,37 +37,6 @@ const AddCalendarButton = () => {
|
|||
|
||||
const CalendarsView = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
||||
|
||||
const [selectedDestinationCalendarOption, setSelectedDestinationCalendar] = useState<{
|
||||
integration: string;
|
||||
externalId: string;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (query?.data?.destinationCalendar) {
|
||||
setSelectedDestinationCalendar({
|
||||
integration: query.data.destinationCalendar.integration,
|
||||
externalId: query.data.destinationCalendar.externalId,
|
||||
});
|
||||
}
|
||||
}, [query?.isLoading, query?.data?.destinationCalendar]);
|
||||
|
||||
const mutation = trpc.viewer.setDestinationCalendar.useMutation({
|
||||
async onSettled() {
|
||||
await utils.viewer.connectedCalendars.invalidate();
|
||||
},
|
||||
onSuccess: async () => {
|
||||
showToast(t("calendar_updated_successfully"), "success");
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("unexpected_error_try_again"), "error");
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -102,167 +46,9 @@ const CalendarsView = () => {
|
|||
CTA={<AddCalendarButton />}
|
||||
borderInShellHeader={false}
|
||||
/>
|
||||
<QueryCell
|
||||
query={query}
|
||||
customLoader={<SkeletonLoader />}
|
||||
success={({ data }) => {
|
||||
const isDestinationUpdateBtnDisabled =
|
||||
selectedDestinationCalendarOption?.externalId === query?.data?.destinationCalendar?.externalId;
|
||||
return data.connectedCalendars.length ? (
|
||||
<div>
|
||||
<div className="border-subtle mt-8 rounded-t-lg border px-4 py-6 sm:px-6">
|
||||
<h2 className="text-emphasis mb-1 text-base font-bold leading-5 tracking-wide">
|
||||
{t("add_to_calendar")}
|
||||
</h2>
|
||||
<p className="text-subtle text-sm leading-tight">{t("add_to_calendar_description")}</p>
|
||||
</div>
|
||||
<div className="border-subtle flex w-full flex-col space-y-3 border border-x border-y-0 px-4 py-6 sm:px-6">
|
||||
<div>
|
||||
<Label className="text-default mb-0 font-medium">{t("add_events_to")}</Label>
|
||||
<DestinationCalendarSelector
|
||||
hidePlaceholder
|
||||
value={selectedDestinationCalendarOption?.externalId}
|
||||
onChange={(option) => {
|
||||
setSelectedDestinationCalendar(option);
|
||||
}}
|
||||
isLoading={mutation.isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<SectionBottomActions align="end">
|
||||
<Button
|
||||
loading={mutation.isLoading}
|
||||
disabled={isDestinationUpdateBtnDisabled}
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
if (selectedDestinationCalendarOption) mutation.mutate(selectedDestinationCalendarOption);
|
||||
}}>
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
|
||||
<div className="border-subtle mt-8 rounded-t-lg border px-4 py-6 sm:px-6">
|
||||
<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>
|
||||
|
||||
<List
|
||||
className="border-subtle flex flex-col gap-6 rounded-b-lg border border-t-0 p-6"
|
||||
noBorderTreatment>
|
||||
{data.connectedCalendars.map((item) => (
|
||||
<Fragment key={item.credentialId}>
|
||||
{item.error && item.error.message && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
key={item.credentialId}
|
||||
title={t("calendar_connection_fail")}
|
||||
message={item.error.message}
|
||||
className="mb-4 mt-4"
|
||||
actions={
|
||||
<>
|
||||
{/* @TODO: add a reconnect button, that calls add api and delete old credential */}
|
||||
<DisconnectIntegration
|
||||
credentialId={item.credentialId}
|
||||
trashIcon
|
||||
onSuccess={() => query.refetch()}
|
||||
buttonProps={{
|
||||
className: "border border-default py-[2px]",
|
||||
color: "secondary",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{item?.error === undefined && item.calendars && (
|
||||
<ListItem className="flex-col rounded-lg">
|
||||
<div className="flex w-full flex-1 items-center space-x-3 p-4 rtl:space-x-reverse">
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
item.integration.logo && (
|
||||
<img
|
||||
className={classNames(
|
||||
"h-10 w-10",
|
||||
item.integration.logo.includes("-dark") && "dark:invert"
|
||||
)}
|
||||
src={item.integration.logo}
|
||||
alt={item.integration.title}
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div className="flex-grow truncate pl-2">
|
||||
<ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse">
|
||||
<Link href={`/apps/${item.integration.slug}`}>
|
||||
{item.integration.name || item.integration.title}
|
||||
</Link>
|
||||
{data?.destinationCalendar?.credentialId === item.credentialId && (
|
||||
<Badge variant="green">Default</Badge>
|
||||
)}
|
||||
</ListItemTitle>
|
||||
<ListItemText component="p">{item.integration.description}</ListItemText>
|
||||
</div>
|
||||
<div>
|
||||
<DisconnectIntegration
|
||||
trashIcon
|
||||
credentialId={item.credentialId}
|
||||
buttonProps={{ className: "border border-default" }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-subtle w-full border-t">
|
||||
<p className="text-subtle px-2 pt-4 text-sm">{t("toggle_calendars_conflict")}</p>
|
||||
<ul className="space-y-4 p-4">
|
||||
{item.calendars.map((cal) => (
|
||||
<CalendarSwitch
|
||||
key={cal.externalId}
|
||||
credentialId={cal.credentialId}
|
||||
externalId={cal.externalId}
|
||||
title={cal.name || "Nameless calendar"}
|
||||
name={cal.name || "Nameless calendar"}
|
||||
type={item.integration.type}
|
||||
isChecked={
|
||||
cal.isSelected || cal.externalId === data?.destinationCalendar?.externalId
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</ListItem>
|
||||
)}
|
||||
</Fragment>
|
||||
))}
|
||||
</List>
|
||||
</div>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={Calendar}
|
||||
headline={t("no_calendar_installed")}
|
||||
description={t("no_calendar_installed_description")}
|
||||
buttonText={t("add_a_calendar")}
|
||||
buttonOnClick={() => router.push("/apps/categories/calendar")}
|
||||
className="mt-6"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
error={() => {
|
||||
return (
|
||||
<Alert
|
||||
message={
|
||||
<Trans i18nKey="fetching_calendars_error">
|
||||
An error ocurred while fetching your Calendars.
|
||||
<a className="cursor-pointer underline" onClick={() => query.refetch()}>
|
||||
try again
|
||||
</a>
|
||||
.
|
||||
</Trans>
|
||||
}
|
||||
severity="error"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
<div className="mt-8">
|
||||
<CalendarListContainer />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -37,6 +37,7 @@ export default function DisconnectIntegration({
|
|||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.connectedCalendars.invalidate();
|
||||
await utils.viewer.integrations.invalidate();
|
||||
},
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue
Block a user