Merge branch 'main' into feature/booking-filters

This commit is contained in:
zomars 2022-12-19 17:46:59 -07:00
commit 1e11260b52
21 changed files with 222 additions and 205 deletions

View File

@ -248,7 +248,7 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</Dialog>
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row">
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
@ -275,11 +275,6 @@ function BookingListItem(booking: BookingItemProps) {
{booking.eventType.team.name}
</Badge>
)}
{!!booking?.eventType?.price && !booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
{t("pending_payment")}
</Badge>
)}
{booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
{t("paid")}
@ -343,7 +338,9 @@ function BookingListItem(booking: BookingItemProps) {
<span> </span>
{!!booking?.eventType?.price && !booking.paid && (
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
<Badge className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex" variant="orange">
{t("pending_payment")}
</Badge>
)}
</div>
{booking.description && (
@ -531,13 +528,4 @@ const DisplayAttendees = ({
);
};
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
return (
<span
className={`inline-flex items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ${className}`}>
{children}
</span>
);
};
export default BookingListItem;

View File

@ -1,6 +1,9 @@
import { UserPermissionRole } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
@ -32,14 +35,33 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
return (
<>
<main className="flex items-center bg-gray-100 print:h-full">
<WizardForm href="/auth/setup" steps={steps} />
<WizardForm
href="/auth/setup"
steps={steps}
nextLabel={t("next_step_text")}
finishLabel={t("finish")}
prevLabel={t("prev_step")}
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
/>
</main>
</>
);
}
export const getServerSideProps = async () => {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const userCount = await prisma.user.count();
const { req } = context;
const session = await getSession({ req });
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
return {
redirect: {
destination: `/404`,
permanent: false,
},
};
}
return {
props: {
userCount,

View File

@ -258,7 +258,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const calLink = `${CAL_URL}/${embedLink}`;
return (
<li key={type.id}>
<div className="flex w-full items-center justify-between hover:bg-neutral-50">
<div className="flex w-full items-center justify-between hover:bg-gray-50">
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
{!(firstItem && firstItem.id === type.id) && (
<button

View File

@ -14,7 +14,7 @@ function Teams() {
heading={t("teams")}
subtitle={t("create_manage_teams_collaborative")}
CTA={
<Button type="button" href={`${WEBAPP_URL}/settings/teams/new`}>
<Button type="button" href={`${WEBAPP_URL}/settings/teams/new?returnTo=/teams`}>
<Icon.FiPlus className="inline-block h-3.5 w-3.5 text-white group-hover:text-black ltr:mr-2 rtl:ml-2" />
{t("new")}
</Button>

View File

@ -1461,6 +1461,8 @@
"event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default",
"hide_eventtype_details": "Hide EventType Details",
"show_navigation": "Show navigation",
"hide_navigation": "Hide navigation",
"verification_code_sent": "Verification code sent",
"verified_successfully": "Verified successfully",
"wrong_code": "Wong verification code",

View File

@ -9,17 +9,17 @@ import { Avatar, Button, Form, Icon, ImageUploader, TextField } from "@calcom/ui
import { NewTeamFormValues } from "../lib/types";
const querySchema = z.optional(z.string());
const querySchema = z.object({
returnTo: z.string(),
});
export const CreateANewTeamForm = () => {
const { t } = useLocale();
const router = useRouter();
const {
query: { returnTo },
} = router;
const returnToParsed = querySchema.safeParse(returnTo);
const returnToParam = returnToParsed.success ? returnToParsed.data : "/settings/teams";
const returnToParsed = querySchema.safeParse(router.query);
const returnToParam = returnToParsed.success ? returnToParsed.data.returnTo : "/settings/teams";
const newTeamFormMethods = useForm<NewTeamFormValues>();

View File

@ -16,9 +16,11 @@ export function TeamsUpgradeBanner() {
showToast(error.message, "error");
},
});
if (!data) return null;
const [membership] = data;
if (!membership) return null;
return (
<TopBanner
text={t("team_upgrade_banner_description", { teamName: membership.team.name })}

View File

@ -46,7 +46,25 @@ const MembersView = () => {
return (
<>
<Meta title={t("team_members")} description={t("members_team_description")} />
<Meta
title={t("team_members")}
description={t("members_team_description")}
CTA={
isAdmin ? (
<Button
type="button"
color="primary"
StartIcon={Icon.FiPlus}
className="ml-auto"
onClick={() => setShowMemberInvitationModal(true)}
data-testid="new-member-button">
{t("add")}
</Button>
) : (
<></>
)
}
/>
{!isLoading && (
<>
<div>
@ -68,19 +86,6 @@ const MembersView = () => {
)}
</>
)}
{isAdmin && (
<div className="relative mb-5 flex w-full items-center ">
<Button
type="button"
color="primary"
StartIcon={Icon.FiPlus}
className="ml-auto"
onClick={() => setShowMemberInvitationModal(true)}
data-testid="new-member-button">
{t("add")}
</Button>
</div>
)}
<div>
<ul className="divide-y divide-gray-200 rounded-md border ">
{team?.members.map((member) => {

View File

@ -4,17 +4,18 @@ import { TFunction } from "next-i18next";
import { TIME_UNIT, WORKFLOW_ACTIONS, WORKFLOW_TEMPLATES, WORKFLOW_TRIGGER_EVENTS } from "./constants";
export function getWorkflowActionOptions(t: TFunction, isTeamsPlan?: boolean) {
return WORKFLOW_ACTIONS.map((action) => {
const actionString = t(`${action.toLowerCase()}_action`);
return WORKFLOW_ACTIONS.filter((action) => action !== WorkflowActions.EMAIL_ADDRESS) //removing EMAIL_ADDRESS for now due to abuse episode
.map((action) => {
const actionString = t(`${action.toLowerCase()}_action`);
const isSMSAction = action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.SMS_NUMBER;
const isSMSAction = action === WorkflowActions.SMS_ATTENDEE || action === WorkflowActions.SMS_NUMBER;
return {
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
value: action,
disabled: isSMSAction && !isTeamsPlan,
};
});
return {
label: actionString.charAt(0).toUpperCase() + actionString.slice(1),
value: action,
disabled: isSMSAction && !isTeamsPlan,
};
});
}
export function getWorkflowTriggerOptions(t: TFunction) {

View File

@ -2,6 +2,10 @@ import { useRouter } from "next/router";
import { useCallback, useMemo } from "react";
import { z } from "zod";
type OptionalKeys<T> = { [K in keyof T]-?: Record<string, unknown> extends Pick<T, K> ? K : never }[keyof T];
type FilteredKeys<T, U> = { [K in keyof T as T[K] extends U ? K : never]: T[K] };
// Take array as a string and return zod array
export const queryNumberArray = z
.string()
@ -20,22 +24,19 @@ export const queryStringArray = z
.preprocess((a) => z.string().parse(a).split(","), z.string().array())
.or(z.string().array());
export function useTypedQuery<T extends z.Schema>(schema: T) {
type InferedSchema = z.infer<typeof schema>;
type SchemaKeys = keyof InferedSchema;
type OptionalKeys = {
[K in keyof InferedSchema]: undefined extends InferedSchema[K] ? K : never;
}[keyof InferedSchema];
type ArrayOnlyKeys = {
[K in keyof InferedSchema]: InferedSchema[K] extends Array<unknown> & undefined ? K : never;
};
export function useTypedQuery<T extends z.ZodType>(schema: T) {
type Output = z.infer<typeof schema>;
type FullOutput = Required<Output>;
type OutputKeys = Required<keyof FullOutput>;
type OutputOptionalKeys = OptionalKeys<Output>;
type ArrayOutput = FilteredKeys<FullOutput, Array<unknown>>;
type ArrayOutputKeys = keyof ArrayOutput;
const { query: unparsedQuery, ...router } = useRouter();
const parsedQuerySchema = schema.safeParse(unparsedQuery);
let parsedQuery: InferedSchema = useMemo(() => {
return {} as InferedSchema;
let parsedQuery: Output = useMemo(() => {
return {} as Output;
}, []);
if (parsedQuerySchema.success) parsedQuery = parsedQuerySchema.data;
@ -43,29 +44,24 @@ export function useTypedQuery<T extends z.Schema>(schema: T) {
// Set the query based on schema values
const setQuery = useCallback(
function setQuery<J extends SchemaKeys>(key: J, value: Partial<InferedSchema[J]>) {
function setQuery<J extends OutputKeys>(key: J, value: Output[J]) {
// Remove old value by key so we can merge new value
const { [key]: _, ...newQuery } = parsedQuery;
const newValue = { ...newQuery, [key]: value };
const search = new URLSearchParams(newValue).toString();
const search = new URLSearchParams(newValue as any).toString();
router.replace({ query: search }, undefined, { shallow: true });
},
[parsedQuery, router]
);
// Delete a key from the query
function removeByKey(key: OptionalKeys) {
function removeByKey(key: OutputOptionalKeys) {
const { [key]: _, ...newQuery } = parsedQuery;
router.replace({ query: newQuery }, undefined, { shallow: true });
router.replace({ query: newQuery as Output }, undefined, { shallow: true });
}
// push item to existing key
function pushItemToKey<J extends SchemaKeys>(
key: J,
value: InferedSchema[J] extends Array<unknown> | undefined
? NonNullable<InferedSchema[J]>[number]
: NonNullable<InferedSchema[J]>
) {
function pushItemToKey<J extends ArrayOutputKeys>(key: J, value: ArrayOutput[J][number]) {
const existingValue = parsedQuery[key];
if (Array.isArray(existingValue)) {
if (existingValue.includes(value)) return; // prevent adding the same value to the array
@ -76,21 +72,14 @@ export function useTypedQuery<T extends z.Schema>(schema: T) {
}
// Remove item by key and value
function removeItemByKeyAndValue<J extends SchemaKeys>(
key: J,
value: InferedSchema[J] extends Array<unknown> | undefined
? NonNullable<InferedSchema[J]>[number]
: NonNullable<InferedSchema[J]>
) {
function removeItemByKeyAndValue<J extends ArrayOutputKeys>(key: J, value: ArrayOutput[J][number]) {
const existingValue = parsedQuery[key];
console.log(existingValue);
const newValue = existingValue.filter((item: InferedSchema[J][number]) => item !== value);
if (Array.isArray(existingValue) && newValue.length > 0) {
setQuery(key, value);
if (Array.isArray(existingValue)) {
// @ts-expect-error this is too much for TS it seems
const newValue = existingValue.filter((item) => item !== value);
setQuery(key, newValue);
} else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore - we know the key is optional but i can't figure out for the life of me
// how to support it in the type
// @ts-expect-error this is too much for TS it seems
removeByKey(key);
}
}

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ALTER COLUMN "seatsShowAttendees" SET DEFAULT false;

View File

@ -68,7 +68,7 @@ model EventType {
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
seatsPerTimeSlot Int?
seatsShowAttendees Boolean?
seatsShowAttendees Boolean? @default(false)
schedulingType SchedulingType?
schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?

View File

@ -9,7 +9,7 @@ const badgeClassNameByVariant = {
orange: "bg-orange-100 text-orange-800",
success: "bg-green-100 text-green-800",
green: "bg-green-100 text-green-800",
gray: "bg-gray-100 text-gray-800 dark:bg-transparent dark:text-darkgray-800",
gray: "bg-gray-100 text-gray-800 dark:bg-transparent dark:text-darkgray-800 group-hover:bg-gray-200",
blue: "bg-blue-100 text-blue-800",
red: "bg-red-100 text-red-800",
error: "bg-red-100 text-red-800",

View File

@ -4,6 +4,8 @@ import classNames from "classnames";
import noop from "lodash/noop";
import { ReactNode } from "react";
import { Icon } from "@calcom/ui";
export type TopBannerProps = {
text: string;
variant?: keyof typeof variantClassName;
@ -23,17 +25,20 @@ export function TopBanner(props: TopBannerProps) {
<div
data-testid="banner"
className={classNames(
"flex min-h-[40px] w-full items-start justify-between gap-8 bg-gray-50 py-2 px-4 text-center lg:items-center",
"flex min-h-[40px] w-full items-start justify-between gap-8 py-2 px-4 text-center lg:items-center",
variantClassName[variant]
)}>
<div className="flex flex-1 flex-col items-start justify-center gap-2 p-1 lg:flex-row lg:items-center">
<p className="flex flex-col items-start justify-center gap-2 text-left font-sans text-sm font-medium leading-4 text-gray-900 lg:flex-row lg:items-center">
{["warning", "error"].includes(variant) && (
<ExclamationIcon className="h-5 w-5 text-black" aria-hidden="true" />
{variant === "error" && (
<Icon.FiAlertTriangle className="h-4 w-4 stroke-[2.5px] text-black" aria-hidden="true" />
)}
{variant === "warning" && (
<Icon.FiInfo className="h-4 w-4 stroke-[2.5px] text-black" aria-hidden="true" />
)}
{text}
</p>
{actions && <div className="text-sm">{actions}</div>}
{actions && <div className="text-sm font-medium">{actions}</div>}
</div>
{typeof onClose === "function" && (
<button

View File

@ -54,14 +54,14 @@ const MeetingTimeInTimezones = ({
<Popover.Root>
<Popover.Trigger
onClick={preventBubbling}
className="popover-button ml-2 inline-flex h-5 w-5 items-center justify-center rounded-sm text-gray-900 transition-colors hover:bg-gray-200 focus:bg-gray-200">
className="popover-button invisible ml-2 inline-flex h-5 w-5 items-center justify-center rounded-sm text-gray-900 transition-colors hover:bg-gray-200 focus:bg-gray-200 group-hover:visible">
<Icon.FiGlobe />
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
onClick={preventBubbling}
side="top"
className="popover-content slideInBottom shadow-dropdown border-5 bg-brand-500 rounded-md border-gray-200 p-3 text-sm text-white shadow-sm">
className="popover-content slideInBottom border-5 bg-brand-500 rounded-md border-gray-200 p-3 text-sm text-white shadow-sm">
{times.map((time) => (
<span className="mt-2 block first:mt-0" key={time.timezone}>
<span className="inline-flex align-baseline">

View File

@ -46,7 +46,7 @@ export default function Meta({ title, description, backButton, CTA }: MetaType)
/* @TODO: maybe find a way to have this data on first render to prevent flicker */
useEffect(() => {
if (meta.title !== title || meta.description !== description) {
if (meta.title !== title || meta.description !== description || meta.CTA !== CTA) {
setMeta({ title, description, backButton, CTA });
}
// eslint-disable-next-line react-hooks/exhaustive-deps

View File

@ -153,12 +153,14 @@ const Layout = (props: LayoutProps) => {
{/* todo: only run this if timezone is different */}
<TimezoneChangeDialog />
<div className="h-screen overflow-hidden">
<div className="flex h-screen overflow-hidden" data-testid="dashboard-shell">
<div className="flex min-h-screen flex-col">
<div className="divide-y divide-black">
<TeamsUpgradeBanner />
<ImpersonatingBanner />
</div>
<div className="flex flex-1" data-testid="dashboard-shell">
{props.SidebarContainer || <SideBarContainer />}
<div className="flex w-0 flex-1 flex-col overflow-hidden">
<TeamsUpgradeBanner />
<ImpersonatingBanner />
<div className="flex w-0 flex-1 flex-col">
<MainContainer {...props} />
</div>
</div>
@ -177,7 +179,6 @@ type LayoutProps = {
children: ReactNode;
CTA?: ReactNode;
large?: boolean;
SettingsSidebarContainer?: ReactNode;
MobileNavigationContainer?: ReactNode;
SidebarContainer?: ReactNode;
TopNavContainer?: ReactNode;
@ -540,7 +541,7 @@ const { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMor
Record<string, NavigationItemType[]>
>(
(items, item, index) => {
// We filter out the "more" separator in desktop navigation
// We filter out the "more" separator in` desktop navigation
if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item);
// Items for mobile bottom navigation
if (index < moreSeparatorIndex + 1 && !item.onlyDesktop) items.mobileNavigationBottomItems.push(item);
@ -714,9 +715,6 @@ const MobileNavigationMoreItem: React.FC<{
};
function DeploymentInfo() {
const query = useMeQuery();
const user = query.data;
return (
<small
style={{
@ -742,59 +740,59 @@ function SideBarContainer() {
function SideBar() {
return (
<aside className="desktop-transparent hidden w-14 flex-col border-r border-gray-100 bg-gray-50 md:flex lg:w-56 lg:flex-shrink-0 lg:px-4">
<div className="flex h-0 flex-1 flex-col overflow-y-auto pt-3 pb-4 lg:pt-5">
<header className="items-center justify-between md:hidden lg:flex">
<div className="relative">
<aside className="desktop-transparent top-0 hidden h-full max-h-screen w-14 flex-col overflow-y-auto border-r border-gray-100 bg-gray-50 md:sticky md:flex lg:w-56 lg:px-4">
<div className="flex flex-col pt-3 pb-32 lg:pt-5">
<header className="items-center justify-between md:hidden lg:flex">
<Link href="/event-types">
<a className="px-4">
<Logo small />
</a>
</Link>
<div className="flex space-x-2">
<button
color="minimal"
onClick={() => window.history.back()}
className="desktop-only group flex text-sm font-medium text-neutral-500 hover:text-neutral-900">
<Icon.FiArrowLeft className="h-4 w-4 flex-shrink-0 text-neutral-500 group-hover:text-neutral-900" />
</button>
<button
color="minimal"
onClick={() => window.history.forward()}
className="desktop-only group flex text-sm font-medium text-neutral-500 hover:text-neutral-900">
<Icon.FiArrowRight className="h-4 w-4 flex-shrink-0 text-neutral-500 group-hover:text-neutral-900" />
</button>
<KBarTrigger />
</div>
</header>
<hr className="desktop-only absolute -left-3 -right-3 mt-4 block w-full border-gray-200" />
{/* logo icon for tablet */}
<Link href="/event-types">
<a className="px-4">
<Logo small />
<a className="text-center md:inline lg:hidden">
<Logo small icon />
</a>
</Link>
<div className="flex space-x-2">
<button
color="minimal"
onClick={() => window.history.back()}
className="desktop-only group flex text-sm font-medium text-neutral-500 hover:text-neutral-900">
<Icon.FiArrowLeft className="h-4 w-4 flex-shrink-0 text-neutral-500 group-hover:text-neutral-900" />
</button>
<button
color="minimal"
onClick={() => window.history.forward()}
className="desktop-only group flex text-sm font-medium text-neutral-500 hover:text-neutral-900">
<Icon.FiArrowRight className="h-4 w-4 flex-shrink-0 text-neutral-500 group-hover:text-neutral-900" />
</button>
<KBarTrigger />
<Navigation />
</div>
{isCalcom && <Tips />}
<div className="fixed left-1 bottom-0 w-4 bg-gray-50 before:absolute before:left-0 before:-top-20 before:h-20 before:w-48 before:bg-gradient-to-t before:from-gray-50 before:to-transparent lg:left-4 lg:w-48">
<div data-testid="user-dropdown-trigger">
<span className="hidden lg:inline">
<UserDropdown />
</span>
<span className="hidden md:inline lg:hidden">
<UserDropdown small />
</span>
</div>
</header>
<hr className="desktop-only absolute -left-3 -right-3 mt-4 block w-full border-gray-200" />
{/* logo icon for tablet */}
<Link href="/event-types">
<a className="text-center md:inline lg:hidden">
<Logo small icon />
</a>
</Link>
<Navigation />
</div>
{isCalcom && <Tips />}
{/* Save it for next preview version
<div className="hidden mb-4 lg:block">
<UserV2OptInBanner />
</div> */}
<div data-testid="user-dropdown-trigger">
<span className="hidden lg:inline">
<UserDropdown />
</span>
<span className="hidden md:inline lg:hidden">
<UserDropdown small />
</span>
</div>
<DeploymentInfo />
</aside>
<DeploymentInfo />
</div>
</aside>
</div>
);
}
@ -855,35 +853,17 @@ export function ShellMain(props: LayoutProps) {
);
}
const SettingsSidebarContainerDefault = () => null;
function MainContainer({
SettingsSidebarContainer: SettingsSidebarContainerProp = <SettingsSidebarContainerDefault />,
MobileNavigationContainer: MobileNavigationContainerProp = <MobileNavigationContainer />,
TopNavContainer: TopNavContainerProp = <TopNavContainer />,
...props
}: LayoutProps) {
const [sideContainerOpen, setSideContainerOpen] = props.drawerState || [false, noop];
return (
<main className="relative z-0 flex flex-1 flex-col overflow-y-auto bg-white focus:outline-none">
<main className="relative z-0 flex-1 bg-white focus:outline-none">
{/* show top navigation for md and smaller (tablet and phones) */}
{TopNavContainerProp}
{/* The following is used for settings navigation on medium and smaller screens */}
<div
className={classNames(
"overflow-none fixed z-40 m-0 h-screen w-screen overscroll-none bg-black opacity-50",
sideContainerOpen ? "" : "hidden"
)}
onClick={() => {
setSideContainerOpen(false);
}}
/>
{SettingsSidebarContainerProp}
<div className="max-w-full px-4 py-2 lg:py-8 lg:px-12">
<ErrorBoundary>
{/* add padding to top for mobile when App Bar is fixed */}
<div className="pt-14 sm:hidden" />
{!props.withoutMain ? <ShellMain {...props}>{props.children}</ShellMain> : props.children}
</ErrorBoundary>
{/* show bottom navigation for md and smaller (tablet and phones) on pages where back button doesn't exist */}
@ -906,7 +886,7 @@ function TopNav() {
<>
<nav
style={isEmbed ? { display: "none" } : {}}
className="fixed z-40 flex w-full items-center justify-between border-b border-gray-200 bg-gray-50 bg-opacity-50 py-1.5 px-4 backdrop-blur-lg sm:relative sm:p-4 md:hidden">
className="sticky top-0 z-40 flex w-full items-center justify-between border-b border-gray-200 bg-gray-50 bg-opacity-50 py-1.5 px-4 backdrop-blur-lg sm:p-4 md:hidden">
<Link href="/event-types">
<a>
<Logo />

View File

@ -1,7 +1,5 @@
import Link from "next/link";
import { useLocale } from "@calcom/lib/hooks/useLocale";
type DefaultStep = {
title: string;
};
@ -11,16 +9,18 @@ function Stepper<T extends DefaultStep>(props: {
step: number;
steps: T[];
disableSteps?: boolean;
stepLabel?: (currentStep: number, totalSteps: number) => string;
}) {
const { t } = useLocale();
const { href, steps } = props;
const {
href,
steps,
stepLabel = (currentStep, totalSteps) => `Step ${currentStep} of ${totalSteps}`,
} = props;
return (
<>
{steps.length > 1 && (
<nav className="flex items-center justify-center" aria-label="Progress">
<p className="text-sm font-medium">
{t("current_step_of_total", { currentStep: props.step, maxSteps: steps.length })}
</p>
<p className="text-sm font-medium">{stepLabel(props.step, steps.length)}</p>
<ol role="list" className="ml-8 flex items-center space-x-5">
{steps.map((mapStep, index) => (
<li key={mapStep.title}>

View File

@ -1,7 +1,7 @@
import { useRouter } from "next/router";
import { ComponentProps } from "react";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Stepper } from "../..";
@ -18,9 +18,12 @@ function WizardForm<T extends DefaultStep>(props: {
steps: T[];
disableNavigation?: boolean;
containerClassname?: string;
prevLabel?: string;
nextLabel?: string;
finishLabel?: string;
stepLabel?: ComponentProps<typeof Stepper>["stepLabel"];
}) {
const { href, steps } = props;
const { t } = useLocale();
const { href, steps, nextLabel = "Next", finishLabel = "Finish", prevLabel = "Back", stepLabel } = props;
const router = useRouter();
const step = parseInt((router.query.step as string) || "1");
const currentStep = steps[step - 1];
@ -53,7 +56,7 @@ function WizardForm<T extends DefaultStep>(props: {
onClick={() => {
setStep(step - 1);
}}>
{t("prev_step")}
{prevLabel}
</Button>
)}
@ -63,11 +66,8 @@ function WizardForm<T extends DefaultStep>(props: {
type="submit"
color="primary"
form={`wizard-step-${step}`}
className="relative ml-2"
onClick={() => {
setStep(step + 1);
}}>
{step < steps.length ? t("next_step_text") : t("finish")}
className="relative ml-2">
{step < steps.length ? nextLabel : finishLabel}
</Button>
</div>
)}
@ -76,7 +76,7 @@ function WizardForm<T extends DefaultStep>(props: {
</div>
{!props.disableNavigation && (
<div className="print:hidden">
<Stepper href={href} step={step} steps={steps} disableSteps />
<Stepper href={href} step={step} steps={steps} disableSteps stepLabel={stepLabel} />
</div>
)}
</div>

View File

@ -100,7 +100,15 @@ const useTabs = () => {
});
};
const SettingsSidebarContainer = ({ className = "" }) => {
interface SettingsSidebarContainerProps {
className?: string;
navigationIsOpenedOnMobile?: boolean;
}
const SettingsSidebarContainer = ({
className = "",
navigationIsOpenedOnMobile,
}: SettingsSidebarContainerProps) => {
const { t } = useLocale();
const router = useRouter();
const tabsWithPermissions = useTabs();
@ -127,7 +135,13 @@ const SettingsSidebarContainer = ({ className = "" }) => {
return (
<nav
className={`no-scrollbar flex w-56 flex-col space-y-1 overflow-x-hidden overflow-y-scroll py-3 px-2 ${className}`}
className={classNames(
"no-scrollbar fixed left-0 top-0 z-10 flex max-h-screen w-56 flex-col space-y-1 overflow-x-hidden overflow-y-scroll bg-gray-50 py-3 px-2 transition-transform lg:sticky lg:flex",
className,
navigationIsOpenedOnMobile
? "translate-x-0 opacity-100"
: "-translate-x-full opacity-0 lg:translate-x-0 lg:opacity-100"
)}
aria-label="Tabs">
<>
<div className="desktop-only pt-4" />
@ -295,9 +309,12 @@ const MobileSettingsContainer = (props: { onSideContainerOpen?: () => void }) =>
return (
<>
<nav className="fixed z-20 flex w-full items-center justify-between border-b border-gray-100 bg-gray-50 p-4 sm:relative lg:hidden">
<nav className="sticky top-0 z-20 flex w-full items-center justify-between border-b border-gray-100 bg-gray-50 p-4 sm:relative lg:hidden">
<div className="flex items-center space-x-3 ">
<Button StartIcon={Icon.FiMenu} color="minimal" size="icon" onClick={props.onSideContainerOpen} />
<Button StartIcon={Icon.FiMenu} color="minimal" size="icon" onClick={props.onSideContainerOpen}>
<span className="sr-only">{t("show_navigation")}</span>
</Button>
<a href="/" className="flex items-center space-x-2 rounded-md px-3 py-1 hover:bg-gray-200">
<Icon.FiArrowLeft className="text-gray-700" />
<p className="font-semibold text-black">{t("settings")}</p>
@ -314,6 +331,7 @@ export default function SettingsLayout({
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
const router = useRouter();
const state = useState(false);
const { t } = useLocale();
const [sideContainerOpen, setSideContainerOpen] = state;
useEffect(() => {
@ -340,18 +358,21 @@ export default function SettingsLayout({
withoutSeo={true}
flexChildrenContainer
{...rest}
SidebarContainer={<SettingsSidebarContainer className="hidden lg:flex" />}
SidebarContainer={
<>
{/* Mobile backdrop */}
{sideContainerOpen && (
<button
onClick={() => setSideContainerOpen(false)}
className="fixed top-0 left-0 z-10 h-full w-full bg-black/50">
<span className="sr-only">{t("hide_navigation")}</span>
</button>
)}
<SettingsSidebarContainer navigationIsOpenedOnMobile={sideContainerOpen} />
</>
}
drawerState={state}
MobileNavigationContainer={null}
SettingsSidebarContainer={
<div
className={classNames(
"fixed inset-y-0 z-50 m-0 h-screen w-56 transform overflow-x-hidden overflow-y-scroll border-gray-100 bg-gray-50 transition duration-200 ease-in-out",
sideContainerOpen ? "translate-x-0" : "-translate-x-full"
)}>
<SettingsSidebarContainer />
</div>
}
TopNavContainer={
<MobileSettingsContainer onSideContainerOpen={() => setSideContainerOpen(!sideContainerOpen)} />
}>
@ -371,7 +392,7 @@ function ShellHeader() {
const { meta } = useMeta();
const { t, isLocaleReady } = useLocale();
return (
<header className="mx-auto block justify-between pt-12 sm:flex sm:pt-8">
<header className="mx-auto block justify-between pt-8 sm:flex">
<div className="mb-8 flex w-full items-center border-b border-gray-200 pb-8">
{meta.backButton && (
<a href="javascript:history.back()">

View File

@ -35,7 +35,7 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
<>
<div className={classNames("dark:text-darkgray-800 text-neutral-500", className)}>
{eventType.description && (
<p className="dark:text-darkgray-800 max-w-[280px] break-words py-2 text-sm text-gray-600 opacity-60 sm:max-w-[500px]">
<p className="dark:text-darkgray-800 max-w-[280px] break-words py-1 text-sm text-gray-500 sm:max-w-[500px]">
{eventType.description.substring(0, 300)}
{eventType.description.length > 300 && "..."}
</p>