EventTypeFilter UI working
This commit is contained in:
parent
1a26a254ab
commit
d078378303
|
@ -5,9 +5,10 @@ import { Fragment } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||||
|
import BookingLayout from "@calcom/features/bookings/layout/BookingLayout";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||||
import { Alert, BookingLayout, Button, EmptyScreen, Icon } from "@calcom/ui";
|
import { Alert, Button, EmptyScreen, Icon } from "@calcom/ui";
|
||||||
|
|
||||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||||
|
|
||||||
|
@ -163,7 +164,7 @@ export default function Bookings() {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{query.status === "success" && isEmpty && (
|
{query.status === "success" && isEmpty && (
|
||||||
<div className="flex items-center justify-center pt-2 xl:mx-6 xl:pt-0">
|
<div className="flex items-center justify-center pt-2 xl:pt-0">
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={Icon.FiCalendar}
|
Icon={Icon.FiCalendar}
|
||||||
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
||||||
|
|
|
@ -1392,5 +1392,6 @@
|
||||||
"options": "Options",
|
"options": "Options",
|
||||||
"enter_option": "Enter Option {{index}}",
|
"enter_option": "Enter Option {{index}}",
|
||||||
"add_an_option": "Add an option",
|
"add_an_option": "Add an option",
|
||||||
"radio": "Radio"
|
"radio": "Radio",
|
||||||
|
"individual":"Individual"
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import { Fragment, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc, RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { Avatar, AnimatedPopover } from "@calcom/ui";
|
||||||
|
|
||||||
|
type EventTypes = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
|
||||||
|
type EventType = EventTypes[0];
|
||||||
|
|
||||||
|
type KeySelector<T> = (item: T) => string;
|
||||||
|
|
||||||
|
function groupBy<T>(array: Iterable<T>, keySelector: KeySelector<T>): Record<string, T[]> {
|
||||||
|
return Array.from(array).reduce(
|
||||||
|
(acc: Record<string, T[]>, item: T) => {
|
||||||
|
const key = keySelector(item);
|
||||||
|
if (key in acc) {
|
||||||
|
// found key, push new item into existing array
|
||||||
|
acc[key].push(item);
|
||||||
|
} else {
|
||||||
|
// did not find key, create new array
|
||||||
|
acc[key] = [item];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} // start with empty object
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type GroupedEventTypeState = Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
} | null;
|
||||||
|
id: number;
|
||||||
|
title: string;
|
||||||
|
slug: string;
|
||||||
|
}[]
|
||||||
|
>;
|
||||||
|
|
||||||
|
export const EventTypeFilter = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { data: user } = useSession();
|
||||||
|
const eventTypes = trpc.viewer.eventTypes.listWithTeam.useQuery();
|
||||||
|
const [groupedEventTypes, setGroupedEventTypes] = useState<GroupedEventTypeState>();
|
||||||
|
// Will be handled up the tree to redirect
|
||||||
|
useEffect(() => {
|
||||||
|
if (eventTypes.data) {
|
||||||
|
// Group event types by team
|
||||||
|
const grouped = groupBy<EventType>(
|
||||||
|
eventTypes.data.filter((el) => el.team),
|
||||||
|
(item) => item?.team?.name || ""
|
||||||
|
); // Add the team name
|
||||||
|
const individualEvents = eventTypes.data.filter((el) => !el.team);
|
||||||
|
// push indivdual events to the start of grouped array
|
||||||
|
setGroupedEventTypes({ user_own_event_types: individualEvents, ...grouped });
|
||||||
|
}
|
||||||
|
}, [eventTypes.data, user]);
|
||||||
|
|
||||||
|
if (!user) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatedPopover text={t("event_type")}>
|
||||||
|
<div className="">
|
||||||
|
{groupedEventTypes &&
|
||||||
|
Object.keys(groupedEventTypes).map((teamName) => (
|
||||||
|
<Fragment key={teamName}>
|
||||||
|
<div className="p-4 text-xs font-medium uppercase leading-none text-gray-500">
|
||||||
|
{teamName === "user_own_event_types" ? t("individual") : teamName}
|
||||||
|
</div>
|
||||||
|
{groupedEventTypes[teamName].map((eventType) => (
|
||||||
|
<Fragment key={eventType.id}>
|
||||||
|
<div className="item-center flex px-4 py-[6px]">
|
||||||
|
<p className="block self-center text-sm font-medium text-gray-700">{eventType.title}</p>
|
||||||
|
<div className="ml-auto">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name=""
|
||||||
|
id=""
|
||||||
|
className="text-primary-600 focus:ring-primary-500 mr-2 h-4 w-4 rounded border-gray-300 "
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</AnimatedPopover>
|
||||||
|
);
|
||||||
|
};
|
|
@ -1,36 +1,30 @@
|
||||||
import React, { ComponentProps } from "react";
|
import React, { ComponentProps } from "react";
|
||||||
|
|
||||||
import type { VerticalTabItemProps } from "../";
|
import { HorizontalTabs, Shell } from "@calcom/ui";
|
||||||
import { HorizontalTabs, VerticalTabs } from "../";
|
import { VerticalTabItemProps, HorizontalTabItemProps } from "@calcom/ui/v2";
|
||||||
import { Icon } from "../../../Icon";
|
|
||||||
import Shell from "../Shell";
|
import { EventTypeFilter } from "../components/EventTypeFilter";
|
||||||
import type { HorizontalTabItemProps } from "../navigation/tabs/HorizontalTabItem";
|
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||||
{
|
{
|
||||||
name: "upcoming",
|
name: "upcoming",
|
||||||
href: "/bookings/upcoming",
|
href: "/bookings/upcoming",
|
||||||
icon: Icon.FiCalendar,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "unconfirmed",
|
name: "unconfirmed",
|
||||||
href: "/bookings/unconfirmed",
|
href: "/bookings/unconfirmed",
|
||||||
icon: Icon.FiInbox,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "recurring",
|
name: "recurring",
|
||||||
href: "/bookings/recurring",
|
href: "/bookings/recurring",
|
||||||
icon: Icon.FiRotateCcw,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "past",
|
name: "past",
|
||||||
href: "/bookings/past",
|
href: "/bookings/past",
|
||||||
icon: Icon.FiSunset,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "cancelled",
|
name: "cancelled",
|
||||||
href: "/bookings/cancelled",
|
href: "/bookings/cancelled",
|
||||||
icon: Icon.FiSlash,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -40,12 +34,12 @@ export default function BookingLayout({
|
||||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||||
return (
|
return (
|
||||||
<Shell {...rest}>
|
<Shell {...rest}>
|
||||||
<div className="flex flex-col sm:space-x-2 xl:flex-row">
|
<div className="flex max-w-6xl flex-col sm:space-x-2">
|
||||||
<div className="hidden xl:block">
|
<div className="flex flex-col lg:flex-row">
|
||||||
<VerticalTabs tabs={tabs} sticky />
|
|
||||||
</div>
|
|
||||||
<div className="block xl:hidden">
|
|
||||||
<HorizontalTabs tabs={tabs} />
|
<HorizontalTabs tabs={tabs} />
|
||||||
|
<div className="flex space-x-2">
|
||||||
|
<EventTypeFilter />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<main className="w-full max-w-6xl">{children}</main>
|
<main className="w-full max-w-6xl">{children}</main>
|
||||||
</div>
|
</div>
|
|
@ -352,6 +352,35 @@ export const eventTypesRouter = router({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
listWithTeam: authedProcedure.query(async ({ ctx }) => {
|
||||||
|
return await ctx.prisma.eventType.findMany({
|
||||||
|
where: {
|
||||||
|
OR: [
|
||||||
|
{ userId: ctx.user.id },
|
||||||
|
{
|
||||||
|
team: {
|
||||||
|
members: {
|
||||||
|
some: {
|
||||||
|
userId: ctx.user.id,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
team: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
title: true,
|
||||||
|
slug: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
create: authedProcedure.input(createEventTypeInput).mutation(async ({ ctx, input }) => {
|
create: authedProcedure.input(createEventTypeInput).mutation(async ({ ctx, input }) => {
|
||||||
const { schedulingType, teamId, ...rest } = input;
|
const { schedulingType, teamId, ...rest } = input;
|
||||||
const userId = ctx.user.id;
|
const userId = ctx.user.id;
|
||||||
|
|
|
@ -26,3 +26,4 @@ export {
|
||||||
} from "./form";
|
} from "./form";
|
||||||
export { TopBanner } from "./top-banner";
|
export { TopBanner } from "./top-banner";
|
||||||
export type { TopBannerProps } from "./top-banner";
|
export type { TopBannerProps } from "./top-banner";
|
||||||
|
export { AnimatedPopover } from "./popover/index";
|
||||||
|
|
|
@ -0,0 +1,44 @@
|
||||||
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { Icon } from "@calcom/ui";
|
||||||
|
|
||||||
|
export const AnimatedPopover = ({
|
||||||
|
text,
|
||||||
|
count,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
text: string;
|
||||||
|
count?: number;
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) => {
|
||||||
|
const [open, setOpen] = React.useState(false);
|
||||||
|
return (
|
||||||
|
<Popover.Root onOpenChange={setOpen} modal={true}>
|
||||||
|
<Popover.Trigger asChild>
|
||||||
|
<div
|
||||||
|
className="item-center mb-2 flex h-9 justify-between whitespace-nowrap rounded-md border border-gray-300 py-2 px-3 text-sm
|
||||||
|
placeholder:text-gray-400 hover:cursor-pointer hover:border-gray-400 focus:border-neutral-300
|
||||||
|
focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1">
|
||||||
|
<div className="flex">
|
||||||
|
<div>
|
||||||
|
{text}
|
||||||
|
{count && count > 0 && (
|
||||||
|
<div className="flex h-4 w-4 items-center justify-center rounded-full">{count}</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Icon.FiChevronDown
|
||||||
|
className={classNames("mt-auto ml-2 transition-transform duration-150", open && "rotate-180")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Popover.Trigger>
|
||||||
|
<Popover.Content side="bottom" align="center" asChild>
|
||||||
|
<div className="absolute z-50 mt-2 w-56 -translate-x-3/4 rounded-md bg-white ring-1 ring-black ring-opacity-5 focus-within:outline-none">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover.Root>
|
||||||
|
);
|
||||||
|
};
|
|
@ -24,6 +24,7 @@ export {
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
TextField,
|
TextField,
|
||||||
TopBanner,
|
TopBanner,
|
||||||
|
AnimatedPopover,
|
||||||
} from "./components";
|
} from "./components";
|
||||||
export type { AvatarProps, BadgeProps, ButtonBaseProps, ButtonProps, TopBannerProps } from "./components";
|
export type { AvatarProps, BadgeProps, ButtonBaseProps, ButtonProps, TopBannerProps } from "./components";
|
||||||
export { default as CheckboxField } from "./components/form/checkbox/Checkbox";
|
export { default as CheckboxField } from "./components/form/checkbox/Checkbox";
|
||||||
|
@ -97,7 +98,6 @@ export { ToggleGroup } from "./v2/core/form/ToggleGroup";
|
||||||
export { default as ImageUploader } from "./v2/core/ImageUploader";
|
export { default as ImageUploader } from "./v2/core/ImageUploader";
|
||||||
export { default as AdminLayout, getLayout as getAdminLayout } from "./v2/core/layouts/AdminLayout";
|
export { default as AdminLayout, getLayout as getAdminLayout } from "./v2/core/layouts/AdminLayout";
|
||||||
export { default as AppsLayout } from "./v2/core/layouts/AppsLayout";
|
export { default as AppsLayout } from "./v2/core/layouts/AppsLayout";
|
||||||
export { default as BookingLayout } from "./v2/core/layouts/BookingLayout";
|
|
||||||
export { default as InstalledAppsLayout } from "./v2/core/layouts/InstalledAppsLayout";
|
export { default as InstalledAppsLayout } from "./v2/core/layouts/InstalledAppsLayout";
|
||||||
export { default as SettingsLayout, getLayout as getSettingsLayout } from "./v2/core/layouts/SettingsLayout";
|
export { default as SettingsLayout, getLayout as getSettingsLayout } from "./v2/core/layouts/SettingsLayout";
|
||||||
export { default as WizardLayout, getLayout as getWizardLayout } from "./v2/core/layouts/WizardLayout";
|
export { default as WizardLayout, getLayout as getWizardLayout } from "./v2/core/layouts/WizardLayout";
|
||||||
|
|
|
@ -17,6 +17,7 @@
|
||||||
"@calcom/trpc": "*",
|
"@calcom/trpc": "*",
|
||||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||||
"@radix-ui/react-dialog": "^1.0.0",
|
"@radix-ui/react-dialog": "^1.0.0",
|
||||||
|
"@radix-ui/react-popover": "^1.0.2",
|
||||||
"@radix-ui/react-portal": "^1.0.0",
|
"@radix-ui/react-portal": "^1.0.0",
|
||||||
"@radix-ui/react-select": "^0.1.1",
|
"@radix-ui/react-select": "^0.1.1",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
|
|
Loading…
Reference in New Issue
Block a user