Merge branch 'feature/booking-filters' of https://github.com/calcom/cal.com into feature/booking-filters
This commit is contained in:
commit
216695b69c
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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>();
|
||||
|
||||
|
|
|
@ -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 })}
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.AnyZodObject>(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,54 +44,44 @@ 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
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setQuery(key, [...existingValue, value]);
|
||||
} else {
|
||||
// @ts-expect-error this is too much for TS it seems
|
||||
setQuery(key, [value]);
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
-- AlterTable
|
||||
ALTER TABLE "EventType" ALTER COLUMN "seatsShowAttendees" SET DEFAULT false;
|
|
@ -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?
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
closeComUpsertTeamUser,
|
||||
} from "@calcom/lib/sync/SyncServiceManager";
|
||||
import { availabilityUserSelect } from "@calcom/prisma";
|
||||
import { User } from "@calcom/prisma/client";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
@ -677,16 +676,11 @@ export const viewerTeamsRouter = router({
|
|||
},
|
||||
},
|
||||
});
|
||||
type UserMap = Record<number, typeof teams[number]["members"][number]["user"]>;
|
||||
// flattern users to be unique by id
|
||||
const users = teams
|
||||
.flatMap((t) => t.members)
|
||||
.reduce((acc, m) => {
|
||||
if (!acc.find((u) => u.id === m.user.id)) {
|
||||
acc.push(m.user);
|
||||
}
|
||||
return acc;
|
||||
}, [] as Partial<User>[]);
|
||||
|
||||
return users;
|
||||
.reduce((acc, m) => (m.user.id in acc ? acc : { ...acc, [m.user.id]: m.user }), {} as UserMap);
|
||||
return Object.values(users);
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 />
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()">
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue
Block a user