feat: organization event type filter (#9253)

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
This commit is contained in:
Udit Takkar 2023-06-05 15:10:46 +05:30 committed by GitHub
parent acda675519
commit 20c7fee1a9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 142 additions and 4 deletions

View File

@ -22,7 +22,7 @@ import SkeletonLoader from "@components/booking/SkeletonLoader";
import { ssgInit } from "@server/lib/ssg";
type BookingListingStatus = z.infer<typeof filterQuerySchema>["status"];
type BookingListingStatus = z.infer<NonNullable<typeof filterQuerySchema>>["status"];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
type RecurringInfo = {

View File

@ -11,6 +11,7 @@ import useIntercom from "@calcom/features/ee/support/lib/intercom/useIntercom";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import CreateEventTypeDialog from "@calcom/features/eventtypes/components/CreateEventTypeDialog";
import { DuplicateDialog } from "@calcom/features/eventtypes/components/DuplicateDialog";
import { OrganizationEventTypeFilter } from "@calcom/features/eventtypes/components/OrganizationEventTypeFilter";
import Shell from "@calcom/features/shell/Shell";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@ -44,6 +45,7 @@ import {
HeadSeo,
Skeleton,
Label,
VerticalDivider,
} from "@calcom/ui";
import {
ArrowDown,
@ -780,6 +782,15 @@ const CTA = () => {
);
};
const Actions = () => {
return (
<div className="flex items-center">
<OrganizationEventTypeFilter />
<VerticalDivider />
</div>
);
};
const WithQuery = withQuery(trpc.viewer.eventTypes.getByViewer);
const EventTypesPage = () => {
@ -806,6 +817,7 @@ const EventTypesPage = () => {
heading={t("event_types_page_title")}
hideHeadingOnMobile
subtitle={t("event_types_page_subtitle")}
beforeCTAactions={<Actions />}
CTA={<CTA />}>
<WithQuery
customLoader={<SkeletonLoader />}

View File

@ -231,6 +231,7 @@
"done": "Done",
"all_done": "All done!",
"all_apps": "All",
"yours":"Yours",
"available_apps": "Available Apps",
"check_email_reset_password": "Check your email. We sent you a link to reset your password.",
"finish": "Finish",

View File

@ -6,7 +6,7 @@ import { queryNumberArray, useTypedQuery } from "@calcom/lib/hooks/useTypedQuery
export const filterQuerySchema = z.object({
teamIds: queryNumberArray.optional(),
userIds: queryNumberArray.optional(),
status: z.enum(["upcoming", "recurring", "past", "cancelled", "unconfirmed"]),
status: z.enum(["upcoming", "recurring", "past", "cancelled", "unconfirmed"]).optional(),
eventTypeIds: queryNumberArray.optional(),
});

View File

@ -0,0 +1,118 @@
import { useSession } from "next-auth/react";
import type { ReactNode, InputHTMLAttributes } from "react";
import { useState, forwardRef, Fragment } from "react";
import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import { AnimatedPopover, Avatar } from "@calcom/ui";
import { Layers, User } from "@calcom/ui/components/icon";
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
export type IEventTypeFilter = IEventTypesFilters[0];
export const OrganizationEventTypeFilter = () => {
const { t } = useLocale();
const session = useSession();
const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery();
const [dropdownTitle, setDropdownTitle] = useState<string>(t("all_apps"));
const { data: teams, status } = trpc.viewer.teams.list.useQuery();
const isNotEmpty = !!teams?.length;
return status === "success" ? (
<AnimatedPopover text={dropdownTitle} popoverTriggerClassNames="mb-0">
<CheckboxFieldContainer>
<CheckboxField
id="all-eventtypes-checkbox"
icon={<Layers className="h-4 w-4" />}
checked={dropdownTitle === t("all_apps")}
onChange={(e) => {
removeAllQueryParams();
setDropdownTitle(t("all_apps"));
// TODO: What to do when all event types is unchecked
}}
label={t("all_apps")}
/>
</CheckboxFieldContainer>
<CheckboxFieldContainer>
<CheckboxField
id="all-eventtypes-checkbox"
icon={<User className="h-4 w-4" />}
checked={query.userIds?.includes(session.data?.user.id || 0)}
onChange={(e) => {
setDropdownTitle(t("yours"));
if (e.target.checked) {
pushItemToKey("userIds", session.data?.user.id || 0);
} else if (!e.target.checked) {
removeItemByKeyAndValue("userIds", session.data?.user.id || 0);
}
}}
label={t("yours")}
/>
</CheckboxFieldContainer>
{isNotEmpty && (
<Fragment>
<div className="text-subtle px-4 py-2.5 text-xs font-medium uppercase leading-none">TEAMS</div>
{teams?.map((team) => (
<CheckboxFieldContainer key={team.id}>
<CheckboxField
id={team.name}
label={team.name}
icon={
<Avatar
alt={team?.name}
imageSrc={getPlaceholderAvatar(team.logo, team?.name as string)}
size="xs"
/>
}
checked={query.teamIds?.includes(team.id)}
onChange={(e) => {
setDropdownTitle(team.name);
if (e.target.checked) {
pushItemToKey("teamIds", team.id);
} else if (!e.target.checked) {
removeItemByKeyAndValue("teamIds", team.id);
}
}}
/>
</CheckboxFieldContainer>
))}
</Fragment>
)}
</AnimatedPopover>
) : null;
};
type Props = InputHTMLAttributes<HTMLInputElement> & {
label: string;
icon: ReactNode;
};
const CheckboxField = forwardRef<HTMLInputElement, Props>(({ label, icon, ...rest }, ref) => {
return (
<label className="flex w-full items-center justify-between">
<div className="flex items-center">
<div className="text-default flex h-6 w-6 items-center justify-center ltr:mr-2 rtl:ml-2">{icon}</div>
<span className="text-sm">{label}</span>
</div>
<div className="flex h-5 items-center">
<input
{...rest}
ref={ref}
type="checkbox"
className="text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded hover:cursor-pointer"
/>
</div>
</label>
);
});
const CheckboxFieldContainer = ({ children }: { children: ReactNode }) => {
return <div className="flex items-center px-3 py-2">{children}</div>;
};
CheckboxField.displayName = "CheckboxField";

View File

@ -219,6 +219,7 @@ type LayoutProps = {
withoutSeo?: boolean;
// Gives the ability to include actions to the right of the heading
actions?: JSX.Element;
beforeCTAactions?: JSX.Element;
smallHeading?: boolean;
hideHeadingOnMobile?: boolean;
};
@ -879,6 +880,7 @@ export function ShellMain(props: LayoutProps) {
</p>
)}
</div>
{props.beforeCTAactions}
{props.CTA && (
<div
className={classNames(

View File

@ -9,11 +9,13 @@ import { ChevronDown } from "../icon";
export const AnimatedPopover = ({
text,
count,
popoverTriggerClassNames,
children,
}: {
text: string;
count?: number;
children: React.ReactNode;
popoverTriggerClassNames?: string;
}) => {
const [open, setOpen] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null);
@ -44,7 +46,10 @@ export const AnimatedPopover = ({
<Popover.Trigger asChild>
<div
ref={ref}
className="hover:border-emphasis border-default text-default hover:text-emphasis mb-2 flex h-9 max-h-72 items-center justify-between whitespace-nowrap rounded-md border px-3 py-2 text-sm hover:cursor-pointer focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1">
className={classNames(
"hover:border-emphasis border-default text-default hover:text-emphasis mb-2 flex h-9 max-h-72 items-center justify-between whitespace-nowrap rounded-md border px-3 py-2 text-sm hover:cursor-pointer focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1",
popoverTriggerClassNames
)}>
<div className="max-w-36 flex items-center">
<Tooltip content={text}>
<div className="truncate">
@ -63,7 +68,7 @@ export const AnimatedPopover = ({
<Popover.Content side="bottom" align={align} asChild>
<div
className={classNames(
"bg-default border-default absolute z-50 mt-2 max-h-64 w-56 overflow-y-scroll rounded-md border py-[2px] shadow-sm focus-within:outline-none",
"bg-default border-default scroll-bar absolute z-50 mt-2 max-h-64 w-56 overflow-y-scroll rounded-md border py-[2px] shadow-sm focus-within:outline-none",
align === "end" && "-translate-x-[228px]"
)}>
{children}