V2 Settings Shell - Add Teams Section & UI fixes (#4347)

* Add teams to sidebar and fix UI

* Clean up

* V2 Multi-select (Team Select) (#4324)

* --init

* design improved

* further fine tuning

* more fixes

* removed extra JSX tag

* added story

* NIT

* revert to use of CheckedTeamSelect

* Removes comments

Co-authored-by: Peer Richelsen <peeroke@gmail.com>

* fix: toggle alligment (#4361)

* fix: add checked tranform for switch (#4357)

* fixed input size on mobile, fixed settings (#4360)

* fixing type errors

* Mobile fixes

* Tests fixes

Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
pull/4402/head
Joe Au-Yeung 2022-09-12 16:26:42 -04:00 committed by GitHub
parent ded1fce6fa
commit 7e52e3d295
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 187 additions and 37 deletions

View File

@ -2,27 +2,18 @@ import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
import {
bookFirstEvent,
bookTimeSlot,
selectFirstAvailableTimeSlotNextMonth,
selectSecondAvailableTimeSlotNextMonth,
} from "./lib/testUtils";
test.describe.configure({ mode: "parallel" });
test("dynamic booking", async ({ page, users }) => {
const pro = await users.create();
await pro.login();
const free = await users.create({ plan: "FREE" });
await page.goto(`/${pro.username}+${free.username}`);
test.describe("dynamic booking", () => {
test.beforeEach(async ({ page, users }) => {
const pro = await users.create();
await pro.login();
const free = await users.create({ plan: "FREE" });
await page.goto(`/${pro.username}+${free.username}`);
});
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
test("book an event first day in next month", async ({ page }) => {
await test.step("book an event first day in next month", async () => {
// Click first event type
await page.click('[data-testid="event-type-link"]');
await selectFirstAvailableTimeSlotNextMonth(page);
@ -30,9 +21,7 @@ test.describe("dynamic booking", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
test("can reschedule a booking", async ({ page }) => {
await bookFirstEvent(page);
await test.step("can reschedule a booking", async () => {
// Logged in
await page.goto("/bookings/upcoming");
await page.locator('[data-testid="edit_booking"]').nth(0).click();
@ -54,9 +43,7 @@ test.describe("dynamic booking", () => {
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
test("Can cancel the recently created booking", async ({ page }) => {
await bookFirstEvent(page);
await test.step("Can cancel the recently created booking", async () => {
await page.goto("/bookings/upcoming");
await page.locator('[data-testid="cancel"]').first().click();
await page.waitForNavigation({
@ -72,4 +59,6 @@ test.describe("dynamic booking", () => {
},
});
});
await users.deleteAll();
});

View File

@ -1189,6 +1189,8 @@
"add_new_form": "Add new form",
"form_description": "Create your form to route a booker",
"copy_link_to_form": "Copy link to form",
"add_a_team": "Add a team",
"saml_config": "SAML Config",
"add_webhook_description": "Receive meeting data in real-time when something happens in Cal.com",
"triggers_when": "Triggers when",
"test_webhook": "Please ping test before creating.",
@ -1203,7 +1205,6 @@
"api_key_update_failed": "Error updating API key name",
"embeds_title": "HTML iframe embed",
"embeds_description": "Embed all your event types on your website",
"pending_payment": "Pending payment",
"create_first_api_key": "Create your first API key",
"create_first_api_key_description": "API keys allow other apps to communicate with Cal.com",
"back_to_signin": "Back to sign in",

View File

@ -1,8 +1,11 @@
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
import { useSession } from "next-auth/react";
import React, { ComponentProps, useState } from "react";
import React, { ComponentProps, useEffect, useState } from "react";
import { classNames } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/v2/core/Button";
import ErrorBoundary from "../../../ErrorBoundary";
@ -10,7 +13,7 @@ import { Icon } from "../../../Icon";
import { useMeta } from "../Meta";
import Shell from "../Shell";
import { VerticalTabItemProps } from "../navigation/tabs/VerticalTabItem";
import VerticalTabs, { VerticalTabItem } from "../navigation/tabs/VerticalTabs";
import { VerticalTabItem } from "../navigation/tabs/VerticalTabs";
const tabs: VerticalTabItemProps[] = [
{
@ -91,18 +94,164 @@ const useTabs = () => {
};
const SettingsSidebarContainer = ({ className = "" }) => {
const { t } = useLocale();
const tabsWithPermissions = useTabs();
const [teamMenuState, setTeamMenuState] =
useState<{ teamId: number | undefined; teamMenuOpen: boolean }[]>();
const { data: teams } = trpc.useQuery(["viewer.teams.list"]);
useEffect(() => {
if (teams) {
const teamStates = teams?.map((team) => ({ teamId: team.id, teamMenuOpen: false }));
setTeamMenuState(teamStates);
}
}, [teams]);
return (
<VerticalTabs tabs={tabsWithPermissions} className={`py-3 pl-3 ${className}`}>
<div className="desktop-only pt-4" />
<VerticalTabItem
name="Settings"
href="/"
icon={Icon.FiArrowLeft}
textClassNames="text-md font-medium leading-none text-black"
className="mb-1"
/>
</VerticalTabs>
<nav
className={`no-scrollbar flex w-56 flex-col space-y-1 overflow-scroll py-3 px-2 ${className}`}
aria-label="Tabs">
<>
<div className="desktop-only pt-4" />
<div>
<VerticalTabItem
name="Settings"
href="/"
icon={Icon.FiArrowLeft}
textClassNames="text-md font-medium leading-none text-black"
/>
</div>
{tabsWithPermissions.map((tab) => {
return tab.name !== "teams" ? (
<>
<div>
<div
className="group flex h-9 w-64 flex-row items-center rounded-md px-3 py-[10px] text-sm font-medium leading-none text-gray-600 hover:bg-gray-100 group-hover:text-gray-700 [&[aria-current='page']]:bg-gray-200 [&[aria-current='page']]:text-gray-900"
key={tab.name}>
{tab && tab.icon && (
<tab.icon className="mr-[12px] h-[16px] w-[16px] self-start stroke-[2px] md:mt-0" />
)}
<p>{t(tab.name)}</p>
</div>
</div>
<div className="mt-2">
{tab.children?.map((tab) => (
<VerticalTabItem
key={tab.name}
name={t(tab.name)}
href={tab.href || "/"}
textClassNames="px-3 text-gray-900 font-medium text-sm"
disableChevron
/>
))}
</div>
</>
) : (
<>
<div>
<div
className="group flex h-9 w-64 flex-row items-center rounded-md px-3 py-[10px] text-sm font-medium leading-none text-gray-600 hover:bg-gray-100 group-hover:text-gray-700 [&[aria-current='page']]:bg-gray-200 [&[aria-current='page']]:text-gray-900"
key={tab.name}>
{tab && tab.icon && (
<tab.icon className="mr-[12px] h-[16px] w-[16px] self-start stroke-[2px] md:mt-0" />
)}
<p>{t(tab.name)}</p>
</div>
{teams &&
teamMenuState &&
teams.map((team, index: number) => (
<Collapsible
key={team.id}
open={teamMenuState[index].teamMenuOpen}
onOpenChange={() =>
setTeamMenuState([
...teamMenuState,
(teamMenuState[index] = {
...teamMenuState[index],
teamMenuOpen: !teamMenuState[index].teamMenuOpen,
}),
])
}>
<CollapsibleTrigger>
<div
className="flex h-9 w-64 flex-row items-center rounded-md px-3 py-[10px] text-sm font-medium leading-none hover:bg-gray-100 group-hover:text-gray-700 [&[aria-current='page']]:bg-gray-200 [&[aria-current='page']]:text-gray-900"
onClick={() =>
setTeamMenuState([
...teamMenuState,
(teamMenuState[index] = {
...teamMenuState[index],
teamMenuOpen: !teamMenuState[index].teamMenuOpen,
}),
])
}>
<div className="mr-[13px]">
{teamMenuState[index].teamMenuOpen ? (
<Icon.FiChevronDown />
) : (
<Icon.FiChevronRight />
)}
</div>
{team.logo && (
<img
ref={team.logo}
className=" ml-[12px] mr-[8px] h-[16px] w-[16px] self-start stroke-[2px] md:mt-0"
alt={team.name || "Team logo"}
/>
)}
<p>{team.name}</p>
</div>
</CollapsibleTrigger>
<CollapsibleContent>
<VerticalTabItem
name={t("profile")}
href={`${WEBAPP_URL}/settings/my-account/appearance`}
textClassNames="px-3 text-gray-900 font-medium text-sm"
disableChevron
/>
<VerticalTabItem
name={t("members")}
href={`${WEBAPP_URL}/settings/my-account/appearance`}
textClassNames="px-3 text-gray-900 font-medium text-sm"
disableChevron
/>
{(team.role === "OWNER" || team.role === "ADMIN") && (
<>
<VerticalTabItem
name={t("general")}
href={`${WEBAPP_URL}/settings/my-account/appearance`}
textClassNames="px-3 text-gray-900 font-medium text-sm"
disableChevron
/>
<VerticalTabItem
name={t("appearance")}
href={`${WEBAPP_URL}/settings/my-account/appearance`}
textClassNames="px-3 text-gray-900 font-medium text-sm"
disableChevron
/>
<VerticalTabItem
name={t("saml_config")}
href={`${WEBAPP_URL}/settings/my-account/appearance`}
textClassNames="px-3 text-gray-900 font-medium text-sm"
disableChevron
/>
</>
)}
</CollapsibleContent>
</Collapsible>
))}
<div
className="group flex h-9 w-64 flex-row items-center rounded-md px-3 py-[10px] text-sm font-medium leading-none hover:bg-gray-100 group-hover:text-gray-700 [&[aria-current='page']]:bg-gray-200 [&[aria-current='page']]:text-gray-900"
key={tab.name}>
<Icon.FiPlus className=" mr-[10px] h-[16px] w-[16px] self-start stroke-[2px] md:mt-0" />
<p>{t("add_a_team")}</p>
</div>
</div>
</>
);
})}
</>
</nav>
);
};

View File

@ -19,6 +19,7 @@ export type VerticalTabItemProps = {
className?: string;
isChild?: boolean;
hidden?: boolean;
disableChevron?: boolean;
} & (
| {
/** If you want to change query param tabName as per current tab */
@ -32,7 +33,15 @@ export type VerticalTabItemProps = {
}
);
const VerticalTabItem: FC<VerticalTabItemProps> = ({ name, href, tabName, info, isChild, ...props }) => {
const VerticalTabItem: FC<VerticalTabItemProps> = ({
name,
href,
tabName,
info,
isChild,
disableChevron,
...props
}) => {
const router = useRouter();
const { t } = useLocale();
let newHref = "";
@ -73,12 +82,14 @@ const VerticalTabItem: FC<VerticalTabItemProps> = ({ name, href, tabName, info,
props.className
)}
aria-current={isCurrent ? "page" : undefined}>
{props.icon && <props.icon className="mr-[10px] h-[16px] w-[16px] self-start stroke-[2px]" />}
{props.icon && (
<props.icon className="mr-[10px] h-[16px] w-[16px] self-start stroke-[2px] md:mt-0" />
)}
<div>
<p>{t(name)}</p>
{info && <p className="pt-1 text-xs font-normal">{t(info)}</p>}
</div>
{isCurrent && (
{!disableChevron && isCurrent && (
<div className="ml-auto self-center">
<Icon.FiChevronRight
width={20}