Feature/new design radio area group (#6289)
* Refactor of the RadioAreaGroup component * Move RadioAreaGroup outside of v2/core * Use useTypedQuery * Removed unused (broken) imports * Fix type for Select
This commit is contained in:
@@ -1,79 +0,0 @@
|
||||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
type RadioAreaProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
|
||||
onChange?: (value: string) => void;
|
||||
defaultChecked?: boolean;
|
||||
};
|
||||
|
||||
const RadioArea = (props: RadioAreaProps) => {
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
"border-1 block border p-4 focus:outline-none focus:ring focus:ring-neutral-500",
|
||||
props.checked && "border-brand",
|
||||
props.className
|
||||
)}>
|
||||
<input
|
||||
onChange={(e) => {
|
||||
if (typeof props.onChange === "function") {
|
||||
props.onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
checked={props.checked}
|
||||
className="float-right ml-3 text-neutral-900 focus:ring-neutral-500"
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
type="radio"
|
||||
/>
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
type ChildrenProps = {
|
||||
props: RadioAreaProps;
|
||||
children?: ReactNode;
|
||||
};
|
||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
||||
children: ChildrenProps | ChildrenProps[];
|
||||
name?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
|
||||
const [checkedIdx, setCheckedIdx] = useState<number | null>(null);
|
||||
|
||||
const changeHandler = (value: string, idx: number) => {
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
setCheckedIdx(idx);
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...passThroughProps}>
|
||||
{(Array.isArray(children) ? children : [children]).map((child, idx: number) => {
|
||||
if (checkedIdx === null && child.props.defaultChecked) {
|
||||
setCheckedIdx(idx);
|
||||
}
|
||||
return (
|
||||
<Item
|
||||
{...child.props}
|
||||
key={idx}
|
||||
name={name}
|
||||
checked={idx === checkedIdx}
|
||||
onChange={(value: string) => changeHandler(value, idx)}>
|
||||
{child.props.children}
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = RadioArea;
|
||||
const Group = RadioAreaGroup;
|
||||
|
||||
export { RadioArea, RadioAreaGroup, Item, Group };
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import React from "react";
|
||||
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { RadioArea, RadioAreaGroup } from "@components/ui/form/radio-area/RadioAreaGroup";
|
||||
|
||||
interface OptionProps
|
||||
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface RadioAreaSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
|
||||
options: OptionProps[]; // allow options to be passed programmatically, like options={}
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
options,
|
||||
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(
|
||||
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left text-sm",
|
||||
disabled && "cursor-default bg-gray-200 focus:ring-0 "
|
||||
)}>
|
||||
{getLabel(props.value) ?? placeholder}
|
||||
<Icon.FiChevronDown className="float-right h-5 w-5 text-neutral-500" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<RadioAreaGroup className="space-y-2 text-sm" name={props.name} onChange={props.onChange}>
|
||||
{options.map((option) => (
|
||||
<RadioArea
|
||||
{...option}
|
||||
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}
|
||||
defaultChecked={props.value === option.value}>
|
||||
<strong className="mb-1 block">{option.label}</strong>
|
||||
<p>{option.description}</p>
|
||||
</RadioArea>
|
||||
))}
|
||||
</RadioAreaGroup>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./RadioAreaGroup";
|
||||
export * from "./Select";
|
||||
@@ -1,14 +1,15 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
||||
@@ -53,58 +54,50 @@ interface CreateEventTypeBtnProps {
|
||||
options: EventTypeParent[];
|
||||
}
|
||||
|
||||
const isValidJSONString = (str: string) => {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const locationFormSchema = z.array(
|
||||
z.object({
|
||||
locationType: z.string(),
|
||||
locationAddress: z.string().optional(),
|
||||
displayLocationPublicly: z.boolean().optional(),
|
||||
locationPhoneNumber: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
.optional(),
|
||||
locationLink: z.string().url().optional(), // URL validates as new URL() - which requires HTTPS:// In the input field
|
||||
})
|
||||
);
|
||||
|
||||
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
||||
const querySchema = z.object({
|
||||
eventPage: z.string(),
|
||||
teamId: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||
title: z.string().optional(),
|
||||
slug: z.string().optional(),
|
||||
length: z
|
||||
.union([z.string().transform((val) => +val), z.number()])
|
||||
.optional()
|
||||
.default(15),
|
||||
description: z.string().optional(),
|
||||
schedulingType: z.nativeEnum(SchedulingType).optional(),
|
||||
locations: z
|
||||
.string()
|
||||
.transform((jsonString) => locationFormSchema.parse(JSON.parse(jsonString)))
|
||||
.optional(),
|
||||
});
|
||||
|
||||
const CreateEventTypeDialog = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
// URL encoded params
|
||||
const teamId: number | undefined =
|
||||
typeof router.query.teamId === "string" && router.query.teamId
|
||||
? parseInt(router.query.teamId)
|
||||
: undefined;
|
||||
const pageSlug = router.query.eventPage || props.options[0].slug;
|
||||
const hasTeams = !!props.options.find((option) => option.teamId);
|
||||
const type: string = typeof router.query.type == "string" && router.query.type ? router.query.type : "";
|
||||
const {
|
||||
data: { teamId, eventPage: pageSlug, ...defaultValues },
|
||||
} = useTypedQuery(querySchema);
|
||||
|
||||
const form = useForm<z.infer<typeof createEventTypeInput>>({
|
||||
resolver: zodResolver(createEventTypeInput),
|
||||
defaultValues,
|
||||
});
|
||||
const { setValue, register } = form;
|
||||
|
||||
useEffect(() => {
|
||||
if (!router.isReady) return;
|
||||
|
||||
const title: string =
|
||||
typeof router.query.title === "string" && router.query.title ? router.query.title : "";
|
||||
const length: number =
|
||||
typeof router.query.length === "string" && router.query.length ? parseInt(router.query.length) : 15;
|
||||
const description: string =
|
||||
typeof router.query.description === "string" && router.query.description
|
||||
? router.query.description
|
||||
: "";
|
||||
const slug: string = typeof router.query.slug === "string" && router.query.slug ? router.query.slug : "";
|
||||
const locations =
|
||||
typeof router.query.locations === "string" &&
|
||||
isValidJSONString(decodeURIComponent(router.query.locations))
|
||||
? JSON.parse(decodeURIComponent(router.query.locations))
|
||||
: [];
|
||||
|
||||
setValue("locations", locations);
|
||||
setValue("title", title);
|
||||
setValue("length", length);
|
||||
setValue("description", description);
|
||||
setValue("slug", slug);
|
||||
// If query params change, update the form
|
||||
}, [router.isReady, router.query, setValue]);
|
||||
const { register } = form;
|
||||
|
||||
const createMutation = trpc.viewer.eventTypes.create.useMutation({
|
||||
onSuccess: async ({ eventType }) => {
|
||||
@@ -129,6 +122,142 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
name="new-eventtype"
|
||||
clearQueryParamsOnClose={[
|
||||
"eventPage",
|
||||
"teamId",
|
||||
"type",
|
||||
"description",
|
||||
"title",
|
||||
"length",
|
||||
"slug",
|
||||
"locations",
|
||||
]}>
|
||||
<DialogContent
|
||||
type="creation"
|
||||
className="overflow-y-auto"
|
||||
title={teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
||||
description={t("new_event_type_to_book_description")}>
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-6">
|
||||
{teamId && (
|
||||
<TextField
|
||||
type="hidden"
|
||||
labelProps={{ style: { display: "none" } }}
|
||||
{...register("teamId", { valueAsNumber: true })}
|
||||
value={teamId}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
label={t("title")}
|
||||
placeholder={t("quick_chat")}
|
||||
{...register("title")}
|
||||
onChange={(e) => {
|
||||
form.setValue("title", e?.target.value);
|
||||
if (form.formState.touchedFields["slug"] === undefined) {
|
||||
form.setValue("slug", slugify(e?.target.value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
|
||||
<TextField
|
||||
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
|
||||
required
|
||||
addOnLeading={<>/{pageSlug}/</>}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
|
||||
</>
|
||||
}
|
||||
{...register("slug")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextAreaField
|
||||
label={t("description")}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
{...register("description")}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<TextField
|
||||
type="number"
|
||||
required
|
||||
min="10"
|
||||
placeholder="15"
|
||||
label={t("length")}
|
||||
className="pr-20"
|
||||
{...register("length", { valueAsNumber: true })}
|
||||
addOnSuffix={t("minutes")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{teamId && (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="schedulingType" className="block text-sm font-bold text-gray-700">
|
||||
{t("scheduling_type")}
|
||||
</label>
|
||||
{form.formState.errors.schedulingType && (
|
||||
<Alert
|
||||
className="mt-1"
|
||||
severity="error"
|
||||
message={form.formState.errors.schedulingType.message}
|
||||
/>
|
||||
)}
|
||||
<RadioArea.Group className="mt-1 flex space-x-4">
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.COLLECTIVE}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("collective")}</strong>
|
||||
<p>{t("collective_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.ROUND_ROBIN}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<DialogClose />
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const hasTeams = !!props.options.find((option) => option.teamId);
|
||||
|
||||
// inject selection data into url for correct router history
|
||||
const openModal = (option: EventTypeParent) => {
|
||||
const query = {
|
||||
@@ -195,137 +324,7 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
||||
)}
|
||||
{/* Dialog for duplicate event type */}
|
||||
{router.query.dialog === "duplicate-event-type" && <DuplicateDialog />}
|
||||
{router.query.dialog === "new-eventtype" && (
|
||||
<Dialog
|
||||
name="new-eventtype"
|
||||
clearQueryParamsOnClose={[
|
||||
"eventPage",
|
||||
"teamId",
|
||||
"type",
|
||||
"description",
|
||||
"title",
|
||||
"length",
|
||||
"slug",
|
||||
"locations",
|
||||
]}>
|
||||
<DialogContent
|
||||
type="creation"
|
||||
className="overflow-y-auto"
|
||||
title={teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
||||
description={t("new_event_type_to_book_description")}>
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-6">
|
||||
{teamId && (
|
||||
<TextField
|
||||
type="hidden"
|
||||
labelProps={{ style: { display: "none" } }}
|
||||
{...register("teamId", { valueAsNumber: true })}
|
||||
value={teamId}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
label={t("title")}
|
||||
placeholder={t("quick_chat")}
|
||||
{...register("title")}
|
||||
onChange={(e) => {
|
||||
form.setValue("title", e?.target.value);
|
||||
if (form.formState.touchedFields["slug"] === undefined) {
|
||||
form.setValue("slug", slugify(e?.target.value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
|
||||
<TextField
|
||||
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
|
||||
required
|
||||
addOnLeading={<>/{pageSlug}/</>}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
|
||||
</>
|
||||
}
|
||||
{...register("slug")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextAreaField
|
||||
label={t("description")}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
{...register("description")}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<TextField
|
||||
type="number"
|
||||
required
|
||||
min="10"
|
||||
placeholder="15"
|
||||
label={t("length")}
|
||||
className="pr-20"
|
||||
{...register("length", { valueAsNumber: true })}
|
||||
addOnSuffix={t("minutes")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{teamId && (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="schedulingType" className="block text-sm font-bold text-gray-700">
|
||||
{t("scheduling_type")}
|
||||
</label>
|
||||
{form.formState.errors.schedulingType && (
|
||||
<Alert
|
||||
className="mt-1"
|
||||
severity="error"
|
||||
message={form.formState.errors.schedulingType.message}
|
||||
/>
|
||||
)}
|
||||
<RadioArea.Group
|
||||
{...register("schedulingType")}
|
||||
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
|
||||
className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse">
|
||||
<RadioArea.Item
|
||||
value={SchedulingType.COLLECTIVE}
|
||||
defaultChecked={type === SchedulingType.COLLECTIVE}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("collective")}</strong>
|
||||
<p>{t("collective_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
value={SchedulingType.ROUND_ROBIN}
|
||||
defaultChecked={type === SchedulingType.ROUND_ROBIN}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<DialogClose />
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
{router.query.dialog === "new-eventtype" && <CreateEventTypeDialog />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
import React from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
type RadioAreaProps = React.InputHTMLAttributes<HTMLInputElement>;
|
||||
|
||||
const RadioArea = React.forwardRef<HTMLInputElement, RadioAreaProps>(
|
||||
({ children, className, ...props }, ref) => {
|
||||
return (
|
||||
<label className={classNames("relative flex", className)}>
|
||||
<input
|
||||
ref={ref}
|
||||
className="peer absolute top-[0.9rem] left-3 align-baseline text-neutral-900 focus:ring-neutral-500"
|
||||
type="radio"
|
||||
{...props}
|
||||
/>
|
||||
<div className="peer-checked:border-brand border-1 rounded-md border p-4 pt-3 pl-10 focus:outline-none focus:ring focus:ring-neutral-500">
|
||||
{children}
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange"> {
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
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 (
|
||||
<div className={className} {...passThroughProps}>
|
||||
{childrenWithProps}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RadioAreaGroup.displayName = "RadioAreaGroup";
|
||||
RadioArea.displayName = "RadioArea";
|
||||
|
||||
const Item = RadioArea;
|
||||
const Group = RadioAreaGroup;
|
||||
|
||||
export { RadioArea, RadioAreaGroup, Item, Group };
|
||||
+13
-5
@@ -1,6 +1,7 @@
|
||||
import { ChevronDownIcon } from "@heroicons/react/solid";
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import React from "react";
|
||||
import { FieldValues, Path, UseFormReturn } from "react-hook-form";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -12,15 +13,22 @@ interface OptionProps
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface RadioAreaSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
|
||||
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(props: RadioAreaSelectProps) {
|
||||
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;
|
||||
@@ -41,12 +49,12 @@ export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
|
||||
<ChevronDownIcon className="float-right h-5 w-5 text-neutral-500" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<RadioAreaGroup className="space-y-2 text-sm" name={props.name} onChange={props.onChange}>
|
||||
<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}`}
|
||||
defaultChecked={props.value === option.value}>
|
||||
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}>
|
||||
<strong className="mb-1 block">{option.label}</strong>
|
||||
<p>{option.description}</p>
|
||||
</RadioArea>
|
||||
@@ -114,7 +114,14 @@ export {
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "./v2/core/Dropdown";
|
||||
export { RadioGroup, Radio, Group, RadioField } from "./v2/core/form";
|
||||
export {
|
||||
RadioGroup,
|
||||
/* TODO: solve this conflict -> Select, */
|
||||
Radio,
|
||||
Group,
|
||||
RadioField,
|
||||
} from "./form/radio-area";
|
||||
|
||||
export { BooleanToggleGroupField } from "./v2/core/form/BooleanToggleGroup";
|
||||
export { DateRangePickerLazy as DateRangePicker } from "./v2/core/form/date-range-picker";
|
||||
export { default as DatePickerField } from "./v2/core/form/DatePicker";
|
||||
|
||||
@@ -28,9 +28,18 @@ export function Dialog(props: DialogProps) {
|
||||
if (open) {
|
||||
router.query["dialog"] = name;
|
||||
} else {
|
||||
const query = router.query;
|
||||
clearQueryParamsOnClose.forEach((queryParam) => {
|
||||
delete router.query[queryParam];
|
||||
delete query[queryParam];
|
||||
});
|
||||
router.push(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
query,
|
||||
},
|
||||
undefined,
|
||||
{ shallow: true }
|
||||
);
|
||||
}
|
||||
setOpen(open);
|
||||
};
|
||||
|
||||
@@ -1,10 +1,3 @@
|
||||
export {
|
||||
RadioGroup,
|
||||
/* TODO: solve this conflict -> Select, */
|
||||
Radio,
|
||||
Group,
|
||||
RadioField,
|
||||
} from "./radio-area";
|
||||
export { default as Checkbox } from "../../../components/form/checkbox/Checkbox";
|
||||
export { default as DatePicker } from "./DatePicker";
|
||||
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
type RadioAreaProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
|
||||
onChange?: (value: string) => void;
|
||||
defaultChecked?: boolean;
|
||||
};
|
||||
|
||||
const RadioArea = (props: RadioAreaProps) => {
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
"border-1 block border p-4 focus:outline-none focus:ring focus:ring-neutral-500",
|
||||
props.checked && "border-brand",
|
||||
props.className
|
||||
)}>
|
||||
<input
|
||||
onChange={(e) => {
|
||||
if (typeof props.onChange === "function") {
|
||||
props.onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
checked={props.checked}
|
||||
className="float-right ml-3 text-neutral-900 focus:ring-neutral-500"
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
type="radio"
|
||||
/>
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
type ChildrenProps = {
|
||||
props: RadioAreaProps;
|
||||
children?: ReactNode;
|
||||
};
|
||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
||||
children: ChildrenProps | ChildrenProps[];
|
||||
name?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
|
||||
const [checkedIdx, setCheckedIdx] = useState<number | null>(null);
|
||||
|
||||
const changeHandler = (value: string, idx: number) => {
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
setCheckedIdx(idx);
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...passThroughProps}>
|
||||
{(Array.isArray(children) ? children : [children]).map((child, idx: number) => {
|
||||
if (checkedIdx === null && child.props.defaultChecked) {
|
||||
setCheckedIdx(idx);
|
||||
}
|
||||
return (
|
||||
<Item
|
||||
{...child.props}
|
||||
key={idx}
|
||||
name={name}
|
||||
checked={idx === checkedIdx}
|
||||
onChange={(value: string) => changeHandler(value, idx)}>
|
||||
{child.props.children}
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = RadioArea;
|
||||
const Group = RadioAreaGroup;
|
||||
|
||||
export { RadioArea, RadioAreaGroup, Item, Group };
|
||||
@@ -19,7 +19,7 @@ export {
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuTriggerItem,
|
||||
} from "./Dropdown";
|
||||
export { Checkbox, DatePicker, FormStep, Radio, RadioGroup } from "./form";
|
||||
export { Checkbox, DatePicker, FormStep } from "./form";
|
||||
export { default as LinkIconButton } from "./LinkIconButton";
|
||||
export { default as Loader } from "./Loader";
|
||||
export { default as MeetingTimeInTimezones } from "./MeetingTimeInTimezones";
|
||||
|
||||
Reference in New Issue
Block a user