feat: organization event type filter (#9253)
Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
This commit is contained in:
parent
acda675519
commit
20c7fee1a9
|
@ -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 = {
|
||||
|
|
|
@ -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 />}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
||||
|
|
|
@ -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";
|
|
@ -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(
|
||||
|
|
|
@ -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}
|
||||
|
|
Loading…
Reference in New Issue
Block a user