diff --git a/apps/web/pages/bookings/[status].tsx b/apps/web/pages/bookings/[status].tsx index 24b6cbc948..988325fae9 100644 --- a/apps/web/pages/bookings/[status].tsx +++ b/apps/web/pages/bookings/[status].tsx @@ -1,12 +1,13 @@ import { useAutoAnimate } from "@formkit/auto-animate/react"; import type { GetStaticPaths, GetStaticProps } from "next"; -import { Fragment } from "react"; +import { Fragment, useState } from "react"; import React from "react"; import { z } from "zod"; import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components"; import dayjs from "@calcom/dayjs"; import { getLayout } from "@calcom/features/MainLayout"; +import { FilterToggle } from "@calcom/features/bookings/components/FilterToggle"; import { FiltersContainer } from "@calcom/features/bookings/components/FiltersContainer"; import type { filterQuerySchema } from "@calcom/features/bookings/lib/useFilterQuery"; import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery"; @@ -81,6 +82,7 @@ export default function Bookings() { const { status } = params ? querySchema.parse(params) : { status: "upcoming" as const }; const { t } = useLocale(); const user = useMeQuery().data; + const [isFiltersVisible, setIsFiltersVisible] = useState(false); const query = trpc.viewer.bookings.get.useInfiniteQuery( { @@ -151,12 +153,11 @@ export default function Bookings() { return (
-
+
-
- -
+
+
{query.status === "error" && ( diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 083aa572e2..d6fc498ae3 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1638,6 +1638,7 @@ "individual": "Individual", "all_bookings_filter_label": "All Bookings", "all_users_filter_label": "All Users", + "all_event_types_filter_label": "All Event Types", "your_bookings_filter_label": "Your Bookings", "meeting_url_variable": "Meeting url", "meeting_url_info": "The event meeting conference url", @@ -1868,6 +1869,7 @@ "review_event_type": "Review Event Type", "looking_for_more_analytics": "Looking for more analytics?", "looking_for_more_insights": "Looking for more Insights?", + "filters": "Filters", "add_filter": "Add filter", "remove_filters": "Clear all filters", "select_user": "Select User", diff --git a/packages/features/bookings/BookingMultiFiltersStore.ts b/packages/features/bookings/BookingMultiFiltersStore.ts deleted file mode 100644 index bfbb9ae646..0000000000 --- a/packages/features/bookings/BookingMultiFiltersStore.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { create } from "zustand"; - -import type { SVGComponent } from "@calcom/types/SVGComponent"; -import { User, Link } from "@calcom/ui/components/icon"; - -interface addFilterOption { - StartIcon?: SVGComponent; - label: "people" | "event_type" | "date_range" | "location"; -} - -export type BookingMultiFilterStore = { - addFilterOptions: addFilterOption[]; - toggleOption: (option: addFilterOption) => void; - isFilterActive: (label: addFilterOption["label"]) => boolean; -}; - -export const useBookingMultiFilterStore = create((set, get) => ({ - addFilterOptions: [ - { - label: "people", - StartIcon: User, - }, - { - label: "event_type", - StartIcon: Link, - }, - // { - // label: "date_range", - // StartIcon: Calendar, - // }, - // { - // label: "location", - // StartIcon: MapPin, - // }, - ], - isFilterActive: (label: addFilterOption["label"]) => { - return !get().addFilterOptions.some((option) => option.label === label); - }, - setState: (state: BookingMultiFilterStore) => set(state), - toggleOption: (option: addFilterOption) => { - const availableOptions = get().addFilterOptions; - const foundOption = availableOptions.find((activeOption) => activeOption.label === option.label); - - if (foundOption) { - set({ - addFilterOptions: availableOptions.filter((activeOption) => activeOption.label !== foundOption.label), - }); - } else { - set({ addFilterOptions: [...availableOptions, option] }); - } - }, -})); diff --git a/packages/features/bookings/components/EventTypeFilter.tsx b/packages/features/bookings/components/EventTypeFilter.tsx index 9b1781573c..6d44a8944e 100644 --- a/packages/features/bookings/components/EventTypeFilter.tsx +++ b/packages/features/bookings/components/EventTypeFilter.tsx @@ -1,11 +1,16 @@ import { useSession } from "next-auth/react"; import { Fragment, useState } from "react"; +import { + FilterCheckboxFieldsContainer, + FilterCheckboxField, +} from "@calcom/features/filters/components/TeamsFilter"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import type { RouterOutputs } from "@calcom/trpc/react"; import { trpc } from "@calcom/trpc/react"; import { AnimatedPopover } from "@calcom/ui"; -import { CheckboxField } from "@calcom/ui"; +import { Divider } from "@calcom/ui"; +import { Link } from "@calcom/ui/components/icon"; import { groupBy } from "../groupBy"; import { useFilterQuery } from "../lib/useFilterQuery"; @@ -29,7 +34,7 @@ type GroupedEventTypeState = Record< export const EventTypeFilter = () => { const { t } = useLocale(); const { data: user } = useSession(); - const { data: query, pushItemToKey, removeItemByKeyAndValue } = useFilterQuery(); + const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery(); const [groupedEventTypes, setGroupedEventTypes] = useState(); @@ -56,24 +61,32 @@ export const EventTypeFilter = () => { const getTextForPopover = () => { const eventTypeIds = query.eventTypeIds; if (eventTypeIds) { - return `${t("event_type")}: ${t("number_selected", { count: eventTypeIds.length })}`; + return `${t("number_selected", { count: eventTypeIds.length })}`; } - return `${t("event_type")}: ${t("all")}`; + return `${t("all")}`; }; return ( - -
- {groupedEventTypes && - !isEmpty && - Object.keys(groupedEventTypes).map((teamName) => ( - -
- {teamName === "user_own_event_types" ? t("individual") : teamName} -
- {groupedEventTypes[teamName].map((eventType) => ( -
- + {!isEmpty ? ( + + } + checked={!query.eventTypeIds?.length} + onChange={removeAllQueryParams} + label={t("all_event_types_filter_label")} + /> + + {groupedEventTypes && + Object.keys(groupedEventTypes).map((teamName) => ( + +
+ {teamName === "user_own_event_types" ? t("individual") : teamName} +
+ {groupedEventTypes[teamName].map((eventType) => ( + { if (e.target.checked) { @@ -82,16 +95,15 @@ export const EventTypeFilter = () => { removeItemByKeyAndValue("eventTypeIds", eventType.id); } }} - description={eventType.title} + label={eventType.title} /> -
- ))} -
- ))} - {isEmpty && ( -

{t("no_options_available")}

- )} -
+ ))} + + ))} + + ) : ( +

{t("no_options_available")}

+ )}
); }; diff --git a/packages/features/bookings/components/FilterToggle.tsx b/packages/features/bookings/components/FilterToggle.tsx new file mode 100644 index 0000000000..32ef451a4f --- /dev/null +++ b/packages/features/bookings/components/FilterToggle.tsx @@ -0,0 +1,35 @@ +import type { Dispatch, SetStateAction } from "react"; + +import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Tooltip, Badge, Button } from "@calcom/ui"; +import { Filter } from "@calcom/ui/components/icon"; + +export interface FilterToggleProps { + setIsFiltersVisible: Dispatch>; +} + +export function FilterToggle({ setIsFiltersVisible }: FilterToggleProps) { + const { + data: { teamIds, userIds, eventTypeIds }, + } = useFilterQuery(); + const { t } = useLocale(); + + function toggleFiltersVisibility() { + setIsFiltersVisible((prev) => !prev); + } + + return ( + + ); +} diff --git a/packages/features/bookings/components/FiltersContainer.tsx b/packages/features/bookings/components/FiltersContainer.tsx index 2fe1c93fcc..1c3c19fe4f 100644 --- a/packages/features/bookings/components/FiltersContainer.tsx +++ b/packages/features/bookings/components/FiltersContainer.tsx @@ -1,156 +1,41 @@ -import { useState } from "react"; -import { shallow } from "zustand/shallow"; +import { useAutoAnimate } from "@formkit/auto-animate/react"; +import { PeopleFilter } from "@calcom/features/bookings/components/PeopleFilter"; import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery"; -import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; -import { - TeamsFilter, - FilterCheckboxFieldsContainer, - FilterCheckboxField, -} from "@calcom/features/filters/components/TeamsFilter"; -import { WEBAPP_URL } from "@calcom/lib/constants"; +import { TeamsFilter } from "@calcom/features/filters/components/TeamsFilter"; import { useLocale } from "@calcom/lib/hooks/useLocale"; -import { trpc } from "@calcom/trpc/react"; -import { - Dropdown, - DropdownMenuContent, - DropdownMenuItem, - DropdownItem, - DropdownMenuTrigger, - AnimatedPopover, - Avatar, - FilterSearchField, - Tooltip, - Button, -} from "@calcom/ui"; -import { Plus } from "@calcom/ui/components/icon"; +import { Tooltip, Button } from "@calcom/ui"; -import { useBookingMultiFilterStore } from "../BookingMultiFiltersStore"; import { EventTypeFilter } from "./EventTypeFilter"; -const PeopleFilter = () => { - const { t } = useLocale(); - const orgBranding = useOrgBranding(); - - const { data: query, pushItemToKey, removeItemByKeyAndValue, removeByKey } = useFilterQuery(); - const [searchText, setSearchText] = useState(""); - - const members = trpc.viewer.teams.listMembers.useQuery({}); - - const filteredMembers = members?.data - ?.filter((member) => member.accepted) - ?.filter((member) => - searchText.trim() !== "" - ? member?.name?.toLowerCase()?.includes(searchText.toLowerCase()) || - member?.username?.toLowerCase()?.includes(searchText.toLowerCase()) - : true - ); - - const getTextForPopover = () => { - const userIds = query.userIds; - if (userIds) { - return `${t("people")}: ${t("number_selected", { count: userIds.length })}`; - } - return `${t("people")}: ${t("all")}`; - }; - - return ( - - setSearchText(e.target.value)} placeholder={t("search")} /> - - {filteredMembers?.map((member) => ( - { - if (e.target.checked) { - pushItemToKey("userIds", member.id); - } else if (!e.target.checked) { - removeItemByKeyAndValue("userIds", member.id); - } - }} - icon={ - - } - /> - ))} - {filteredMembers?.length === 0 && ( -

{t("no_options_available")}

- )} -
-
- ); -}; - -export function FiltersContainer() { - const { t } = useLocale(); +export interface FiltersContainerProps { + isFiltersVisible: boolean; +} +export function FiltersContainer({ isFiltersVisible }: FiltersContainerProps) { + const [animationParentRef] = useAutoAnimate(); const { removeAllQueryParams } = useFilterQuery(); - - const [addFilterOptions, toggleOption, isFilterActive] = useBookingMultiFilterStore((state) => [ - state.addFilterOptions, - state.toggleOption, - state.isFilterActive, - shallow, - ]); - - const isPeopleFilterActive = isFilterActive("people"); - const isEventTypeFilterActive = isFilterActive("event_type"); + const { t } = useLocale(); return ( -
- {addFilterOptions.length > 0 && ( - - -
- - -
{t("add_filter")}
-
-
-
- - {addFilterOptions?.map((option) => ( - - { - toggleOption(option); - }}> - {t(option.label)} - - - ))} - -
- )} - - {isPeopleFilterActive && } - {isEventTypeFilterActive && } - {(isPeopleFilterActive || isEventTypeFilterActive) && ( - - - - )} - +
+ {isFiltersVisible ? ( +
+ + + + + + +
+ ) : null}
); } diff --git a/packages/features/bookings/components/PeopleFilter.tsx b/packages/features/bookings/components/PeopleFilter.tsx new file mode 100644 index 0000000000..f5a1666a5a --- /dev/null +++ b/packages/features/bookings/components/PeopleFilter.tsx @@ -0,0 +1,85 @@ +import { useState } from "react"; + +import { useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery"; +import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider"; +import { + FilterCheckboxFieldsContainer, + FilterCheckboxField, +} from "@calcom/features/filters/components/TeamsFilter"; +import { WEBAPP_URL } from "@calcom/lib/constants"; +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { AnimatedPopover, Avatar, Divider, FilterSearchField } from "@calcom/ui"; +import { User } from "@calcom/ui/components/icon"; + +export const PeopleFilter = () => { + const { t } = useLocale(); + const orgBranding = useOrgBranding(); + + const { data: query, pushItemToKey, removeItemByKeyAndValue, removeAllQueryParams } = useFilterQuery(); + const [searchText, setSearchText] = useState(""); + + const members = trpc.viewer.teams.listMembers.useQuery({}); + + const filteredMembers = members?.data + ?.filter((member) => member.accepted) + ?.filter((member) => + searchText.trim() !== "" + ? member?.name?.toLowerCase()?.includes(searchText.toLowerCase()) || + member?.username?.toLowerCase()?.includes(searchText.toLowerCase()) + : true + ); + + const getTextForPopover = () => { + const userIds = query.userIds; + if (userIds) { + return `${t("number_selected", { count: userIds.length })}`; + } + return `${t("all")}`; + }; + + return ( + + + } + checked={!query.userIds?.length} + onChange={removeAllQueryParams} + label={t("all_users_filter_label")} + /> + + setSearchText(e.target.value)} placeholder={t("search")} /> + {filteredMembers?.map((member) => ( + { + if (e.target.checked) { + pushItemToKey("userIds", member.id); + } else if (!e.target.checked) { + removeItemByKeyAndValue("userIds", member.id); + } + }} + icon={ + + } + /> + ))} + {filteredMembers?.length === 0 && ( +

{t("no_options_available")}

+ )} +
+
+ ); +}; diff --git a/packages/features/filters/components/TeamsFilter.tsx b/packages/features/filters/components/TeamsFilter.tsx index 8d52f6f317..91e7b04425 100644 --- a/packages/features/filters/components/TeamsFilter.tsx +++ b/packages/features/filters/components/TeamsFilter.tsx @@ -60,15 +60,16 @@ export const TeamsFilter = ({ return (
- + } checked={!query.teamIds && !query.userIds?.includes(session.data?.user.id || 0)} - onChange={(e) => { - removeAllQueryParams(); - }} + onChange={removeAllQueryParams} label={t("all")} /> @@ -134,7 +135,7 @@ export const FilterCheckboxFieldsContainer = ({ type Props = InputHTMLAttributes & { label: string; - icon: ReactNode; + icon?: ReactNode; }; export const FilterCheckboxField = forwardRef(({ label, icon, ...rest }, ref) => { @@ -142,9 +143,11 @@ export const FilterCheckboxField = forwardRef(({ label,
diff --git a/packages/ui/components/popover/AnimatedPopover.tsx b/packages/ui/components/popover/AnimatedPopover.tsx index 33735a68f8..cd3e6e3b56 100644 --- a/packages/ui/components/popover/AnimatedPopover.tsx +++ b/packages/ui/components/popover/AnimatedPopover.tsx @@ -13,6 +13,7 @@ export const AnimatedPopover = ({ children, Trigger, defaultOpen, + prefix, }: { text: string; count?: number; @@ -20,6 +21,7 @@ export const AnimatedPopover = ({ popoverTriggerClassNames?: string; Trigger?: React.ReactNode; defaultOpen?: boolean; + prefix?: string; }) => { const [open, setOpen] = React.useState(defaultOpen ?? false); const ref = React.useRef(null); @@ -58,8 +60,9 @@ export const AnimatedPopover = ({ Trigger ) : (
- +
+ {prefix && {prefix} } {text} {count && count > 0 && (