Fix/connected calendar component (#3956)
* Fix for displaying connected calendars alert with test * Fix default calendar display string when externalId its too long * Update apps/web/components/ui/LinkText.tsx Co-authored-by: Omar López <zomars@me.com> Co-authored-by: Omar López <zomars@me.com>pull/3905/head^2
parent
5950b62b02
commit
1b541ff214
|
@ -70,7 +70,13 @@ const DestinationCalendarSelector = ({
|
|||
value: `${cal.integration}:${cal.externalId}`,
|
||||
})),
|
||||
})) ?? [];
|
||||
|
||||
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
||||
? `(${
|
||||
destinationCalendar.externalId.length > 15
|
||||
? destinationCalendar.externalId.substring(0, 15) + "..."
|
||||
: destinationCalendar.externalId
|
||||
})`
|
||||
: "";
|
||||
return (
|
||||
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
||||
<Select
|
||||
|
@ -79,8 +85,8 @@ const DestinationCalendarSelector = ({
|
|||
!hidePlaceholder ? (
|
||||
`${t("select_destination_calendar")}`
|
||||
) : (
|
||||
<span>
|
||||
{t("default_calendar_selected")} ({destinationCalendar?.externalId})
|
||||
<span className="whitespace-nowrap">
|
||||
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
@ -104,7 +110,6 @@ const DestinationCalendarSelector = ({
|
|||
borderRadius: "2px",
|
||||
"@media only screen and (min-width: 640px)": {
|
||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
||||
maxWidth: "320px",
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
import Link from "next/link";
|
||||
|
||||
interface ILinkTextProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
classNameChildren?: string;
|
||||
}
|
||||
/**
|
||||
* This component had to be made in order to make i18n work with next/link
|
||||
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
|
||||
**/
|
||||
export const LinkText = (props: ILinkTextProps) => {
|
||||
const { href, children, classNameChildren, ...moreProps } = props;
|
||||
return (
|
||||
<Link href={href || ""} {...moreProps}>
|
||||
<a className={classNameChildren}>{children}</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,40 @@
|
|||
import { cleanup, screen, render } from "@testing-library/react";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
import NoCalendarConnectedAlert from "..";
|
||||
|
||||
describe("Testing NoCalendarConnectedAlert", () => {
|
||||
describe("Render test", () => {
|
||||
it("should render without crashing", () => {
|
||||
// Disabled as its asking for full trpc useQuery response
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
jest.spyOn(trpc, "useQuery").mockReturnValue({
|
||||
isSuccess: true,
|
||||
isFetched: true,
|
||||
data: { connectedCalendars: [], destinationCalendar: undefined },
|
||||
});
|
||||
render(<NoCalendarConnectedAlert />);
|
||||
|
||||
const testProp = screen.getByText("missing_connected_calendar");
|
||||
expect(testProp).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should not render", () => {
|
||||
// Disabled as its asking for full trpc useQuery response
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
jest.spyOn(trpc, "useQuery").mockReturnValue({
|
||||
isSuccess: true,
|
||||
isFetched: true,
|
||||
data: { connectedCalendars: [1, 2], destinationCalendar: { a: "test" } },
|
||||
});
|
||||
render(<NoCalendarConnectedAlert />);
|
||||
|
||||
expect(screen.queryByText("missing_connected_calendar")).toBeNull();
|
||||
});
|
||||
|
||||
afterEach(cleanup);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,44 @@
|
|||
import { useTranslation } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
// @TODO: alert v2
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
|
||||
import { LinkText } from "../LinkText";
|
||||
|
||||
const NoCalendarConnectedAlert = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
const query = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||
// We are not gonna show this alert till we fetch data from DB
|
||||
let defaultCalendarConnected = true;
|
||||
if (query.isSuccess && query.isFetched && query.data) {
|
||||
defaultCalendarConnected =
|
||||
query.data.connectedCalendars.length > 0 && query.data.destinationCalendar !== undefined;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{!defaultCalendarConnected && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="mb-4"
|
||||
title={<>{t("missing_connected_calendar") as string}</>}
|
||||
message={
|
||||
<Trans i18nKey="connect_your_calendar_and_link">
|
||||
You can connect your calendar from
|
||||
<LinkText
|
||||
data-testid="no-calendar-connected-alert"
|
||||
href="/apps/categories/calendar"
|
||||
classNameChildren="underline">
|
||||
here
|
||||
</LinkText>
|
||||
.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
export default NoCalendarConnectedAlert;
|
|
@ -7,7 +7,7 @@ const config: Config.InitialOptions = {
|
|||
verbose: true,
|
||||
roots: ["<rootDir>"],
|
||||
setupFiles: ["<rootDir>/test/jest-setup.js"],
|
||||
testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)"],
|
||||
testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)", "**/__tests__/**/*.(spec|test).(ts|tsx|js)"],
|
||||
testPathIgnorePatterns: ["<rootDir>/.next", "<rootDir>/playwright/"],
|
||||
transform: {
|
||||
"^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }],
|
||||
|
|
|
@ -128,6 +128,7 @@
|
|||
"@calcom/types": "*",
|
||||
"@microsoft/microsoft-graph-types-beta": "0.15.0-preview",
|
||||
"@playwright/test": "^1.25.0",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@types/accept-language-parser": "1.5.2",
|
||||
"@types/async": "^3.2.15",
|
||||
"@types/bcryptjs": "^2.4.2",
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { UserPlan } from "@prisma/client";
|
||||
import { GetServerSidePropsContext, InferGetServerSidePropsType } from "next";
|
||||
import { Trans } from "next-i18next";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
|
@ -9,7 +8,6 @@ import React, { Fragment, useEffect, useState } from "react";
|
|||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import showToast from "@calcom/lib/notification";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Alert } from "@calcom/ui/Alert";
|
||||
|
@ -28,7 +26,6 @@ import Shell from "@calcom/ui/Shell";
|
|||
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||
|
||||
import { withQuery } from "@lib/QueryCell";
|
||||
import { getSession } from "@lib/auth";
|
||||
import classNames from "@lib/classNames";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
|
@ -38,13 +35,13 @@ import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
|||
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||
import { LinkText } from "@components/ui/LinkText";
|
||||
import NoCalendarConnectedAlert from "@components/ui/NoCalendarConnectedAlert";
|
||||
|
||||
import { TRPCClientError } from "@trpc/react";
|
||||
|
||||
type EventTypeGroups = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"];
|
||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||
type ConnectedCalendars = inferQueryOutput<"viewer.connectedCalendars">["connectedCalendars"][number];
|
||||
|
||||
interface EventTypeListHeadingProps {
|
||||
profile: EventTypeGroupProfile;
|
||||
membershipCount: number;
|
||||
|
@ -59,17 +56,7 @@ interface EventTypeListProps {
|
|||
types: EventType[];
|
||||
}
|
||||
|
||||
const Item = ({
|
||||
type,
|
||||
group,
|
||||
readOnly,
|
||||
connectedCalendars,
|
||||
}: {
|
||||
type: EventType;
|
||||
group: EventTypeGroup;
|
||||
readOnly: boolean;
|
||||
connectedCalendars: ConnectedCalendars[] | undefined;
|
||||
}) => {
|
||||
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
|
@ -216,8 +203,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
}
|
||||
}, []);
|
||||
|
||||
const connectedCalendarsQuery = trpc.useQuery(["viewer.connectedCalendars"]);
|
||||
|
||||
return (
|
||||
<div className="-mx-4 mb-16 overflow-hidden rounded-sm border border-gray-200 bg-white sm:mx-0">
|
||||
<ul className="divide-y divide-neutral-200" data-testid="event-types">
|
||||
|
@ -254,12 +239,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
</button>
|
||||
</>
|
||||
)}
|
||||
<MemoizedItem
|
||||
type={type}
|
||||
group={group}
|
||||
readOnly={readOnly}
|
||||
connectedCalendars={connectedCalendarsQuery.data?.connectedCalendars}
|
||||
/>
|
||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||
<div className="mt-4 hidden flex-shrink-0 sm:mt-0 sm:ml-5 sm:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.users?.length > 1 && (
|
||||
|
@ -571,9 +551,8 @@ const CTA = () => {
|
|||
|
||||
const WithQuery = withQuery(["viewer.eventTypes"]);
|
||||
|
||||
const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideProps>) => {
|
||||
const EventTypesPage = () => {
|
||||
const { t } = useLocale();
|
||||
const { defaultCalendarConnected } = props;
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
@ -597,31 +576,17 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
|
|||
message={
|
||||
<Trans i18nKey="plan_upgrade_instructions">
|
||||
You can
|
||||
<a href="/api/upgrade" className="underline">
|
||||
<LinkText href="/api/upgrade" classNameChildren="underline">
|
||||
upgrade here
|
||||
</a>
|
||||
</LinkText>
|
||||
.
|
||||
</Trans>
|
||||
}
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
{!defaultCalendarConnected && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
className="mb-4"
|
||||
title={<>{t("missing_connected_calendar") as string}</>}
|
||||
message={
|
||||
<Trans i18nKey="connect_your_calendar_and_link">
|
||||
You can connect your calendar from
|
||||
<a href="/apps/categories/calendar" className="underline">
|
||||
here
|
||||
</a>
|
||||
.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NoCalendarConnectedAlert />
|
||||
|
||||
{data.eventTypeGroups.map((group, index) => (
|
||||
<Fragment key={group.profile.slug}>
|
||||
|
@ -652,26 +617,4 @@ const EventTypesPage = (props: InferGetServerSidePropsType<typeof getServerSideP
|
|||
);
|
||||
};
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const session = await getSession(context);
|
||||
let defaultCalendarConnected = false;
|
||||
|
||||
if (session && session.user) {
|
||||
const defaultCalendar = await prisma.destinationCalendar.findFirst({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
credentialId: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
defaultCalendarConnected = !!defaultCalendar;
|
||||
}
|
||||
return {
|
||||
props: {
|
||||
defaultCalendarConnected,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export default EventTypesPage;
|
||||
|
|
56
yarn.lock
56
yarn.lock
|
@ -5687,6 +5687,29 @@
|
|||
lodash.isplainobject "^4.0.6"
|
||||
lodash.merge "^4.6.2"
|
||||
|
||||
"@testing-library/dom@^8.5.0":
|
||||
version "8.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/dom/-/dom-8.17.1.tgz#2d7af4ff6dad8d837630fecd08835aee08320ad7"
|
||||
integrity sha512-KnH2MnJUzmFNPW6RIKfd+zf2Wue8mEKX0M3cpX6aKl5ZXrJM1/c/Pc8c2xDNYQCnJO48Sm5ITbMXgqTr3h4jxQ==
|
||||
dependencies:
|
||||
"@babel/code-frame" "^7.10.4"
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@types/aria-query" "^4.2.0"
|
||||
aria-query "^5.0.0"
|
||||
chalk "^4.1.0"
|
||||
dom-accessibility-api "^0.5.9"
|
||||
lz-string "^1.4.4"
|
||||
pretty-format "^27.0.2"
|
||||
|
||||
"@testing-library/react@^13.3.0":
|
||||
version "13.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@testing-library/react/-/react-13.3.0.tgz#bf298bfbc5589326bbcc8052b211f3bb097a97c5"
|
||||
integrity sha512-DB79aA426+deFgGSjnf5grczDPiL4taK3hFaa+M5q7q20Kcve9eQottOG5kZ74KEr55v0tU2CQormSSDK87zYQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.12.5"
|
||||
"@testing-library/dom" "^8.5.0"
|
||||
"@types/react-dom" "^18.0.0"
|
||||
|
||||
"@tootallnate/once@1":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
|
||||
|
@ -5803,6 +5826,11 @@
|
|||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/aria-query@^4.2.0":
|
||||
version "4.2.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-4.2.2.tgz#ed4e0ad92306a704f9fb132a0cfcf77486dbe2bc"
|
||||
integrity sha512-HnYpAE1Y6kRyKM/XkEuiRQhTHvkzMBurTHnpFLYLBGPIylZNPs9jJcuOOYWxPLJCSEtmZT0Y8rHDokKN7rRTig==
|
||||
|
||||
"@types/asn1js@^2.0.2":
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/asn1js/-/asn1js-2.0.2.tgz#bb1992291381b5f06e22a829f2ae009267cdf8c5"
|
||||
|
@ -6314,7 +6342,7 @@
|
|||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react-dom@18.0.4":
|
||||
"@types/react-dom@18.0.4", "@types/react-dom@^18.0.0":
|
||||
version "18.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.0.4.tgz#dcbcadb277bcf6c411ceff70069424c57797d375"
|
||||
integrity sha512-FgTtbqPOCI3dzZPZoC2T/sx3L34qxy99ITWn4eoSA95qPyXDMH0ALoAqUp49ITniiJFsXUVBtalh/KffMpg21Q==
|
||||
|
@ -7370,6 +7398,11 @@ aria-query@^4.2.2:
|
|||
"@babel/runtime" "^7.10.2"
|
||||
"@babel/runtime-corejs3" "^7.10.2"
|
||||
|
||||
aria-query@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.0.0.tgz#210c21aaf469613ee8c9a62c7f86525e058db52c"
|
||||
integrity sha512-V+SM7AbUwJ+EBnB8+DXs0hPZHO0W6pqBcc0dW90OwtVG02PswOu/teuARoLQjdDOH+t9pJgGnW5/Qmouf3gPJg==
|
||||
|
||||
arr-diff@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/arr-diff/-/arr-diff-4.0.0.tgz#d6461074febfec71e7e15235761a329a5dc7c520"
|
||||
|
@ -10136,6 +10169,11 @@ doctrine@^2.1.0:
|
|||
dependencies:
|
||||
esutils "^2.0.2"
|
||||
|
||||
dom-accessibility-api@^0.5.9:
|
||||
version "0.5.14"
|
||||
resolved "https://registry.yarnpkg.com/dom-accessibility-api/-/dom-accessibility-api-0.5.14.tgz#56082f71b1dc7aac69d83c4285eef39c15d93f56"
|
||||
integrity sha512-NMt+m9zFMPZe0JcY9gN224Qvk6qLIdqex29clBvc/y75ZBX9YA9wNK3frsYvu2DI1xcCIwxwnX+TlsJ2DSOADg==
|
||||
|
||||
dom-converter@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/dom-converter/-/dom-converter-0.2.0.tgz#6721a9daee2e293682955b6afe416771627bb768"
|
||||
|
@ -15620,6 +15658,11 @@ luxon@^1.26.0:
|
|||
resolved "https://registry.yarnpkg.com/luxon/-/luxon-1.28.0.tgz#e7f96daad3938c06a62de0fb027115d251251fbf"
|
||||
integrity sha512-TfTiyvZhwBYM/7QdAVDh+7dBTBA29v4ik0Ce9zda3Mnf8on1S5KJI8P2jKFZ8+5C0jhmr0KwJEO/Wdpm0VeWJQ==
|
||||
|
||||
lz-string@^1.4.4:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
|
||||
integrity sha512-0ckx7ZHRPqb0oUm8zNr+90mtf9DQB60H1wMCjBtfi62Kl3a7JbHob6gA2bC+xRvZoOL+1hzUK8jeuEIQE8svEQ==
|
||||
|
||||
magic-string@^0.26.1, magic-string@^0.26.2:
|
||||
version "0.26.2"
|
||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.26.2.tgz#5331700e4158cd6befda738bb6b0c7b93c0d4432"
|
||||
|
@ -18626,6 +18669,15 @@ pretty-error@^4.0.0:
|
|||
lodash "^4.17.20"
|
||||
renderkid "^3.0.0"
|
||||
|
||||
pretty-format@^27.0.2:
|
||||
version "27.5.1"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-27.5.1.tgz#2181879fdea51a7a5851fb39d920faa63f01d88e"
|
||||
integrity sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==
|
||||
dependencies:
|
||||
ansi-regex "^5.0.1"
|
||||
ansi-styles "^5.0.0"
|
||||
react-is "^17.0.1"
|
||||
|
||||
pretty-format@^28.0.0, pretty-format@^28.1.3:
|
||||
version "28.1.3"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-28.1.3.tgz#c9fba8cedf99ce50963a11b27d982a9ae90970d5"
|
||||
|
@ -19286,7 +19338,7 @@ react-intl@^5.25.1:
|
|||
intl-messageformat "9.13.0"
|
||||
tslib "^2.1.0"
|
||||
|
||||
react-is@17.0.2, react-is@^17.0.2:
|
||||
react-is@17.0.2, react-is@^17.0.1, react-is@^17.0.2:
|
||||
version "17.0.2"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0"
|
||||
integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==
|
||||
|
|
Loading…
Reference in New Issue