Compare commits
3 Commits
main
...
gh-readonl
Author | SHA1 | Date | |
---|---|---|---|
4090e46bb1 | |||
c46e90d776 | |||
ee996192ff |
|
@ -39,10 +39,10 @@ test.describe("Managed Event Types tests", () => {
|
||||||
await page.click("[data-testid=new-event-type-dropdown]");
|
await page.click("[data-testid=new-event-type-dropdown]");
|
||||||
await page.click("[data-testid=option-team-1]");
|
await page.click("[data-testid=option-team-1]");
|
||||||
// Expecting we can add a managed event type as team owner
|
// Expecting we can add a managed event type as team owner
|
||||||
await expect(page.locator('input[value="MANAGED"]')).toBeVisible();
|
await expect(page.locator('button[value="MANAGED"]')).toBeVisible();
|
||||||
|
|
||||||
// Actually creating a managed event type to test things further
|
// Actually creating a managed event type to test things further
|
||||||
await page.click('input[value="MANAGED"]');
|
await page.click('button[value="MANAGED"]');
|
||||||
await page.fill("[name=title]", "managed");
|
await page.fill("[name=title]", "managed");
|
||||||
await page.click("[type=submit]");
|
await page.click("[type=submit]");
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dx": "docker compose up -d"
|
"dx": "docker compose up -d || docker-compose up -d"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@calcom/dayjs": "*",
|
"@calcom/dayjs": "*",
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { Fragment, useState, useEffect } from "react";
|
import { Fragment, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { AnimatedPopover } from "@calcom/ui";
|
import { AnimatedPopover } from "@calcom/ui";
|
||||||
|
import { Checkbox } from "@calcom/ui";
|
||||||
|
|
||||||
import { groupBy } from "../groupBy";
|
import { groupBy } from "../groupBy";
|
||||||
|
import { useFilterQuery } from "../lib/useFilterQuery";
|
||||||
|
|
||||||
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
|
export type IEventTypesFilters = RouterOutputs["viewer"]["eventTypes"]["listWithTeam"];
|
||||||
export type IEventTypeFilter = IEventTypesFilters[0];
|
export type IEventTypeFilter = IEventTypesFilters[0];
|
||||||
|
@ -27,24 +29,32 @@ type GroupedEventTypeState = Record<
|
||||||
export const EventTypeFilter = () => {
|
export const EventTypeFilter = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: user } = useSession();
|
const { data: user } = useSession();
|
||||||
const eventTypes = trpc.viewer.eventTypes.listWithTeam.useQuery();
|
const { data: query, pushItemToKey, removeItemByKeyAndValue } = useFilterQuery();
|
||||||
|
|
||||||
const [groupedEventTypes, setGroupedEventTypes] = useState<GroupedEventTypeState>();
|
const [groupedEventTypes, setGroupedEventTypes] = useState<GroupedEventTypeState>();
|
||||||
// Will be handled up the tree to redirect
|
|
||||||
useEffect(() => {
|
|
||||||
if (!eventTypes.data) return;
|
|
||||||
// Group event types by team
|
|
||||||
const grouped = groupBy<IEventTypeFilter>(
|
|
||||||
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;
|
const eventTypes = trpc.viewer.eventTypes.listWithTeam.useQuery(undefined, {
|
||||||
|
onSuccess: (data) => {
|
||||||
|
// Will be handled up the tree to redirect
|
||||||
|
// Group event types by team
|
||||||
|
const grouped = groupBy<IEventTypeFilter>(
|
||||||
|
data.filter((el) => el.team),
|
||||||
|
(item) => item?.team?.name || ""
|
||||||
|
); // Add the team name
|
||||||
|
const individualEvents = data.filter((el) => !el.team);
|
||||||
|
// push indivdual events to the start of grouped array
|
||||||
|
setGroupedEventTypes(
|
||||||
|
individualEvents.length > 0 ? { user_own_event_types: individualEvents, ...grouped } : grouped
|
||||||
|
);
|
||||||
|
},
|
||||||
|
enabled: !!user,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
if (!eventTypes.data) return null;
|
||||||
|
|
||||||
|
const isNotEmpty = eventTypes.data.length > 0;
|
||||||
|
|
||||||
|
return eventTypes.status === "success" && isNotEmpty ? (
|
||||||
<AnimatedPopover text={t("event_type")}>
|
<AnimatedPopover text={t("event_type")}>
|
||||||
<div className="">
|
<div className="">
|
||||||
{groupedEventTypes &&
|
{groupedEventTypes &&
|
||||||
|
@ -54,25 +64,23 @@ export const EventTypeFilter = () => {
|
||||||
{teamName === "user_own_event_types" ? t("individual") : teamName}
|
{teamName === "user_own_event_types" ? t("individual") : teamName}
|
||||||
</div>
|
</div>
|
||||||
{groupedEventTypes[teamName].map((eventType) => (
|
{groupedEventTypes[teamName].map((eventType) => (
|
||||||
<Fragment key={eventType.id}>
|
<div key={eventType.id} className="item-center flex px-4 py-1.5">
|
||||||
<div className="item-center flex px-4 py-[6px]">
|
<Checkbox
|
||||||
<p className="text-default block self-center truncate text-sm font-medium">
|
checked={query.eventTypeIds?.includes(eventType.id)}
|
||||||
{eventType.title}
|
onChange={(e) => {
|
||||||
</p>
|
if (e.target.checked) {
|
||||||
<div className="ml-auto">
|
pushItemToKey("eventTypeIds", eventType.id);
|
||||||
<input
|
} else if (!e.target.checked) {
|
||||||
type="checkbox"
|
removeItemByKeyAndValue("eventTypeIds", eventType.id);
|
||||||
name=""
|
}
|
||||||
id=""
|
}}
|
||||||
className="text-primary-600 focus:ring-primary-500 border-default h-4 w-4 rounded ltr:mr-2 rtl:ml-2 "
|
description={eventType.title}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</Fragment>
|
|
||||||
))}
|
))}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</AnimatedPopover>
|
</AnimatedPopover>
|
||||||
);
|
) : null;
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
import { Fragment, ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
import { Fragment } from "react";
|
||||||
|
|
||||||
|
import { EventTypeFilter } from "./EventTypeFilter";
|
||||||
import { PeopleFilter } from "./PeopleFilter";
|
import { PeopleFilter } from "./PeopleFilter";
|
||||||
import { TeamsMemberFilter } from "./TeamFilter";
|
import { TeamsMemberFilter } from "./TeamFilter";
|
||||||
|
|
||||||
type FilterTypes = "teams" | "people";
|
type FilterTypes = "teams" | "people" | "eventType";
|
||||||
|
|
||||||
type Filter = {
|
type Filter = {
|
||||||
name: FilterTypes;
|
name: FilterTypes;
|
||||||
|
@ -25,6 +27,12 @@ const filters: Filter[] = [
|
||||||
controllingQueryParams: ["usersId"],
|
controllingQueryParams: ["usersId"],
|
||||||
showByDefault: true,
|
showByDefault: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "eventType",
|
||||||
|
component: <EventTypeFilter />,
|
||||||
|
controllingQueryParams: ["eventTypeId"],
|
||||||
|
showByDefault: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function FiltersContainer() {
|
export function FiltersContainer() {
|
||||||
|
|
|
@ -254,6 +254,9 @@ export default function CreateEventTypeDialog({
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<RadioArea.Group
|
<RadioArea.Group
|
||||||
|
onValueChange={(val: SchedulingType) => {
|
||||||
|
form.setValue("schedulingType", val);
|
||||||
|
}}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mt-1 flex gap-4",
|
"mt-1 flex gap-4",
|
||||||
isAdmin && flags["managed-event-types"] && "flex-col"
|
isAdmin && flags["managed-event-types"] && "flex-col"
|
||||||
|
|
|
@ -102,6 +102,15 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
eventTypeIds: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
eventTypeId: {
|
||||||
|
in: input.filters?.eventTypeIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const filtersCombined: Prisma.BookingWhereInput[] =
|
const filtersCombined: Prisma.BookingWhereInput[] =
|
||||||
|
|
|
@ -1,59 +1,57 @@
|
||||||
import React from "react";
|
import { useId } from "@radix-ui/react-id";
|
||||||
|
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group";
|
||||||
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
|
||||||
type RadioAreaProps = React.InputHTMLAttributes<HTMLInputElement> & { classNames?: { container?: string } };
|
type RadioAreaProps = RadioGroupPrimitive.RadioGroupItemProps & {
|
||||||
|
children: ReactNode;
|
||||||
|
classNames?: { container?: string };
|
||||||
|
};
|
||||||
|
|
||||||
const RadioArea = React.forwardRef<HTMLInputElement, RadioAreaProps>(
|
const RadioArea = ({ children, className, classNames: innerClassNames, ...props }: RadioAreaProps) => {
|
||||||
({ children, className, classNames: innerClassNames, ...props }, ref) => {
|
const radioAreaId = useId();
|
||||||
return (
|
const id = props.id ?? radioAreaId;
|
||||||
<label className={classNames("relative flex", className)}>
|
|
||||||
<input
|
|
||||||
ref={ref}
|
|
||||||
className="text-emphasis bg-subtle border-emphasis focus:ring-none peer absolute top-[0.9rem] left-3 align-baseline"
|
|
||||||
type="radio"
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className={classNames(
|
|
||||||
"text-default peer-checked:border-emphasis border-subtle rounded-md border p-4 pt-3 pl-10",
|
|
||||||
innerClassNames?.container
|
|
||||||
)}>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
type MaybeArray<T> = T[] | T;
|
|
||||||
type ChildrenOfType<T extends React.ElementType> = MaybeArray<
|
|
||||||
React.ReactElement<React.ComponentPropsWithoutRef<T>>
|
|
||||||
>;
|
|
||||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
children: ChildrenOfType<typeof RadioArea>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const RadioAreaGroup = ({ children, className, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
|
|
||||||
const childrenWithProps = React.Children.map(children, (child) => {
|
|
||||||
if (onChange && React.isValidElement(child)) {
|
|
||||||
return React.cloneElement(child, {
|
|
||||||
onChange: (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
onChange(e.target.value);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
});
|
|
||||||
return (
|
return (
|
||||||
<div className={className} {...passThroughProps}>
|
<div
|
||||||
{childrenWithProps}
|
className={classNames(
|
||||||
|
"border-subtle [&:has(input:checked)]:border-emphasis relative flex items-start rounded-md border",
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
<RadioGroupPrimitive.Item
|
||||||
|
id={id}
|
||||||
|
{...props}
|
||||||
|
className={classNames(
|
||||||
|
"hover:bg-subtle border-default focus:ring-emphasis absolute top-[0.9rem] left-3 mt-0.5 h-4 w-4 flex-shrink-0 rounded-full border focus:ring-2",
|
||||||
|
props.disabled && "opacity-60"
|
||||||
|
)}>
|
||||||
|
<RadioGroupPrimitive.Indicator
|
||||||
|
className={classNames(
|
||||||
|
"after:bg-default dark:after:bg-inverted relative flex h-full w-full items-center justify-center rounded-full bg-black after:h-[6px] after:w-[6px] after:rounded-full after:content-['']",
|
||||||
|
props.disabled ? "after:bg-muted" : "bg-black"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</RadioGroupPrimitive.Item>
|
||||||
|
<label htmlFor={id} className={classNames("text-default p-4 pt-3 pl-10", innerClassNames?.container)}>
|
||||||
|
{children}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
RadioAreaGroup.displayName = "RadioAreaGroup";
|
const RadioAreaGroup = ({
|
||||||
RadioArea.displayName = "RadioArea";
|
children,
|
||||||
|
className,
|
||||||
|
onValueChange,
|
||||||
|
...passThroughProps
|
||||||
|
}: RadioGroupPrimitive.RadioGroupProps) => {
|
||||||
|
return (
|
||||||
|
<RadioGroupPrimitive.Root className={className} onValueChange={onValueChange} {...passThroughProps}>
|
||||||
|
{children}
|
||||||
|
</RadioGroupPrimitive.Root>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Item = RadioArea;
|
const Item = RadioArea;
|
||||||
const Group = RadioAreaGroup;
|
const Group = RadioAreaGroup;
|
||||||
|
|
|
@ -1,68 +0,0 @@
|
||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
|
||||||
import React from "react";
|
|
||||||
import type { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { ChevronDown } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
import { RadioArea, RadioAreaGroup } from "./RadioAreaGroup";
|
|
||||||
|
|
||||||
interface OptionProps
|
|
||||||
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
|
|
||||||
description?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type FieldPath<TFieldValues extends FieldValues> = Path<TFieldValues>;
|
|
||||||
interface RadioAreaSelectProps<TFieldValues extends FieldValues>
|
|
||||||
extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange" | "form"> {
|
|
||||||
options: OptionProps[]; // allow options to be passed programmatically, like options={}
|
|
||||||
onChange?: (value: string) => void;
|
|
||||||
form: UseFormReturn<TFieldValues>;
|
|
||||||
name: FieldPath<TFieldValues>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Select = function RadioAreaSelect<TFieldValues extends FieldValues>(
|
|
||||||
props: RadioAreaSelectProps<TFieldValues>
|
|
||||||
) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const {
|
|
||||||
options,
|
|
||||||
form,
|
|
||||||
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
|
|
||||||
placeholder = t("select"),
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
|
|
||||||
options.find((option: OptionProps) => option.value === value)?.label;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Collapsible className={classNames("w-full", props.className)}>
|
|
||||||
<CollapsibleTrigger
|
|
||||||
type="button"
|
|
||||||
disabled={disabled}
|
|
||||||
className={classNames(
|
|
||||||
"focus:ring-primary-500 border-default bg-default mb-1 block w-full cursor-pointer rounded-sm border border p-2 text-left shadow-sm sm:text-sm",
|
|
||||||
disabled && "bg-emphasis cursor-default focus:ring-0 "
|
|
||||||
)}>
|
|
||||||
{getLabel(props.value) ?? placeholder}
|
|
||||||
<ChevronDown className="text-subtle float-right h-5 w-5" />
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
|
||||||
<RadioAreaGroup className="space-y-2 text-sm" onChange={props.onChange}>
|
|
||||||
{options.map((option) => (
|
|
||||||
<RadioArea
|
|
||||||
{...form.register(props.name)}
|
|
||||||
{...option}
|
|
||||||
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}>
|
|
||||||
<strong className="mb-1 block">{option.label}</strong>
|
|
||||||
<p>{option.description}</p>
|
|
||||||
</RadioArea>
|
|
||||||
))}
|
|
||||||
</RadioAreaGroup>
|
|
||||||
</CollapsibleContent>
|
|
||||||
</Collapsible>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Select;
|
|
|
@ -1,3 +1,2 @@
|
||||||
export * as RadioGroup from "./RadioAreaGroup";
|
export * as RadioGroup from "./RadioAreaGroup";
|
||||||
export { default as Select } from "./Select";
|
|
||||||
export { Group, Indicator, Label, Radio, RadioField } from "./Radio";
|
export { Group, Indicator, Label, Radio, RadioField } from "./Radio";
|
||||||
|
|
Loading…
Reference in New Issue
Block a user