Tidy up on event-types + NITS (#4119)
* Apply correct padding to switch * Make gray badges less eye catching * Update description text color * Tidy up setup tab * Use V2 dialog on edit location * use V2 select * Menu Item component + fix menuitem * Fix dropdown alignment
This commit is contained in:
@@ -30,7 +30,7 @@ export const EventTypeDescription = ({ eventType, className }: EventTypeDescript
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={classNames("text-neutral-500 dark:text-white", className)}>
|
||||
<div className={classNames("text-gray-600 dark:text-white", className)}>
|
||||
{eventType.description && (
|
||||
<h2 className="max-w-[280px] overflow-hidden text-ellipsis opacity-60 sm:max-w-[500px]">
|
||||
{eventType.description.substring(0, 100)}
|
||||
|
||||
@@ -15,16 +15,16 @@ import {
|
||||
} from "@calcom/app-store/locations";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
||||
import { Button } from "@calcom/ui/v2";
|
||||
import { Dialog, DialogContent } from "@calcom/ui/v2";
|
||||
import { Button, Label } from "@calcom/ui/v2";
|
||||
import { Form } from "@calcom/ui/v2";
|
||||
import { Select } from "@calcom/ui/v2/";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
||||
|
||||
@@ -161,17 +161,15 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<label htmlFor="locationInput" className="block text-sm font-medium text-gray-700">
|
||||
{t(eventLocationType.messageForOrganizer || "")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<div>
|
||||
<Label htmlFor="locationInput">{t(eventLocationType.messageForOrganizer || "")}</Label>
|
||||
<LocationInput
|
||||
locationFormMethods={locationFormMethods}
|
||||
eventLocationType={eventLocationType}
|
||||
id="locationInput"
|
||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||
required
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
className="block w-full rounded-md border-gray-300 text-sm"
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
defaultValue={
|
||||
(defaultLocation && defaultLocation[eventLocationType.defaultValueVariable]) || ""
|
||||
@@ -211,130 +209,119 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
|
||||
return (
|
||||
<Dialog open={isOpenDialog}>
|
||||
<DialogContent asChild>
|
||||
<div className="inline-block transform rounded-md bg-white px-4 pt-5 pb-4 text-left align-bottom shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6 sm:align-middle">
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
{t("edit_location")}
|
||||
</h3>
|
||||
{!booking && (
|
||||
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left" />
|
||||
</div>
|
||||
{booking && (
|
||||
<>
|
||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
||||
<p className="mb-2 ml-1 text-sm text-black">
|
||||
{getHumanReadableLocationValue(booking.location, t)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<Form
|
||||
form={locationFormMethods}
|
||||
handleSubmit={async (values) => {
|
||||
const { locationType: newLocation, displayLocationPublicly } = values;
|
||||
<DialogContent
|
||||
type="creation"
|
||||
Icon={Icon.FiMapPin}
|
||||
useOwnActionButtons
|
||||
title={t("edit_location")}
|
||||
description={!booking ? t("this_input_will_shown_booking_this_event") : undefined}>
|
||||
{booking && (
|
||||
<>
|
||||
<p className="mt-6 mb-2 ml-1 text-sm font-bold text-black">{t("current_location")}:</p>
|
||||
<p className="mb-2 ml-1 text-sm text-black">
|
||||
{getHumanReadableLocationValue(booking.location, t)}
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
<Form
|
||||
form={locationFormMethods}
|
||||
handleSubmit={async (values) => {
|
||||
const { locationType: newLocation, displayLocationPublicly } = values;
|
||||
|
||||
let details = {};
|
||||
if (newLocation === LocationType.InPerson) {
|
||||
details = {
|
||||
address: values.locationAddress,
|
||||
};
|
||||
}
|
||||
const eventLocationType = getEventLocationType(newLocation);
|
||||
let details = {};
|
||||
if (newLocation === LocationType.InPerson) {
|
||||
details = {
|
||||
address: values.locationAddress,
|
||||
};
|
||||
}
|
||||
const eventLocationType = getEventLocationType(newLocation);
|
||||
|
||||
// TODO: There can be a property that tells if it is to be saved in `link`
|
||||
if (
|
||||
newLocation === LocationType.Link ||
|
||||
(!eventLocationType?.default && eventLocationType?.linkType === "static")
|
||||
) {
|
||||
details = { link: values.locationLink };
|
||||
}
|
||||
// TODO: There can be a property that tells if it is to be saved in `link`
|
||||
if (
|
||||
newLocation === LocationType.Link ||
|
||||
(!eventLocationType?.default && eventLocationType?.linkType === "static")
|
||||
) {
|
||||
details = { link: values.locationLink };
|
||||
}
|
||||
|
||||
if (newLocation === LocationType.UserPhone) {
|
||||
details = { hostPhoneNumber: values.locationPhoneNumber };
|
||||
}
|
||||
if (newLocation === LocationType.UserPhone) {
|
||||
details = { hostPhoneNumber: values.locationPhoneNumber };
|
||||
}
|
||||
|
||||
if (eventLocationType?.organizerInputType) {
|
||||
details = {
|
||||
...details,
|
||||
displayLocationPublicly,
|
||||
};
|
||||
}
|
||||
if (eventLocationType?.organizerInputType) {
|
||||
details = {
|
||||
...details,
|
||||
displayLocationPublicly,
|
||||
};
|
||||
}
|
||||
|
||||
saveLocation(newLocation, details);
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
locationFormMethods.unregister([
|
||||
"locationType",
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
}}>
|
||||
<QueryCell
|
||||
query={locationsQuery}
|
||||
success={({ data: locationOptions }) => {
|
||||
if (!locationOptions.length) return null;
|
||||
return (
|
||||
<Controller
|
||||
name="locationType"
|
||||
control={locationFormMethods.control}
|
||||
render={() => (
|
||||
<Select
|
||||
maxMenuHeight={150}
|
||||
name="location"
|
||||
defaultValue={selection}
|
||||
options={
|
||||
booking
|
||||
? locationOptions.filter((location) => location.value !== "phone")
|
||||
: locationOptions
|
||||
saveLocation(newLocation, details);
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
locationFormMethods.unregister([
|
||||
"locationType",
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
}}>
|
||||
<QueryCell
|
||||
query={locationsQuery}
|
||||
success={({ data: locationOptions }) => {
|
||||
if (!locationOptions.length) return null;
|
||||
return (
|
||||
<Controller
|
||||
name="locationType"
|
||||
control={locationFormMethods.control}
|
||||
render={() => (
|
||||
<Select
|
||||
maxMenuHeight={150}
|
||||
name="location"
|
||||
defaultValue={selection}
|
||||
options={
|
||||
booking
|
||||
? locationOptions.filter((location) => location.value !== "phone")
|
||||
: locationOptions
|
||||
}
|
||||
isSearchable={false}
|
||||
className="my-4 block w-full min-w-0 flex-1 rounded-md border border-gray-300 text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
locationFormMethods.unregister([
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
locationFormMethods.clearErrors([
|
||||
"locationLink",
|
||||
"locationPhoneNumber",
|
||||
"locationAddress",
|
||||
]);
|
||||
setSelectedLocation?.(val);
|
||||
}
|
||||
isSearchable={false}
|
||||
className="my-4 block w-full min-w-0 flex-1 rounded-md border border-gray-300 text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
locationFormMethods.unregister([
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
locationFormMethods.clearErrors([
|
||||
"locationLink",
|
||||
"locationPhoneNumber",
|
||||
"locationAddress",
|
||||
]);
|
||||
setSelectedLocation?.(val);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
{selectedLocation && LocationOptions}
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
locationFormMethods.unregister("locationType");
|
||||
}}
|
||||
/>
|
||||
{selectedLocation && LocationOptions}
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
locationFormMethods.unregister("locationType");
|
||||
}}
|
||||
type="button"
|
||||
color="secondary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit">{t("update")}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
type="button"
|
||||
color="secondary">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button type="submit">{t("update")}</Button>
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -4,21 +4,17 @@ import { SchedulingType } from "@prisma/client";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { EventTypeSetupInfered, FormValues } from "pages/v2/event-types/[type]";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Controller, useForm, useFormContext, useWatch } from "react-hook-form";
|
||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getEventLocationType, EventLocationType } from "@calcom/app-store/locations";
|
||||
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import { Select, Label, TextField, Portal, Switch } from "@calcom/ui/v2";
|
||||
import * as RadioArea from "@calcom/ui/v2/core/form/radio-area";
|
||||
import { Select, Label, TextField } from "@calcom/ui/v2";
|
||||
|
||||
import { asStringOrUndefined } from "@lib/asStringOrNull";
|
||||
import { slugify } from "@lib/slugify";
|
||||
|
||||
import CheckedSelect from "@components/ui/form/CheckedSelect";
|
||||
import { EditLocationDialog } from "@components/v2/eventtype/EditLocationDialog";
|
||||
|
||||
type OptionTypeBase = {
|
||||
@@ -32,8 +28,7 @@ export const EventSetupTab = (
|
||||
) => {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { eventType, locationOptions, team, teamMembers } = props;
|
||||
const utils = trpc.useContext();
|
||||
const { eventType, locationOptions, team } = props;
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
|
||||
|
||||
@@ -42,17 +37,6 @@ export const EventSetupTab = (
|
||||
setShowLocationModal(true);
|
||||
};
|
||||
|
||||
const setHiddenMutation = trpc.useMutation("viewer.eventTypes.update", {
|
||||
onError: async (err) => {
|
||||
console.error(err.message);
|
||||
await utils.cancelQuery(["viewer.eventTypes"]);
|
||||
await utils.invalidateQueries(["viewer.eventTypes"]);
|
||||
},
|
||||
onSettled: async () => {
|
||||
await utils.invalidateQueries(["viewer.eventTypes"]);
|
||||
},
|
||||
});
|
||||
|
||||
const removeLocation = (selectedLocation: typeof eventType.locations[number]) => {
|
||||
formMethods.setValue(
|
||||
"locations",
|
||||
@@ -79,37 +63,6 @@ export const EventSetupTab = (
|
||||
setShowLocationModal(false);
|
||||
};
|
||||
|
||||
const schedulingTypeOptions: {
|
||||
value: SchedulingType;
|
||||
label: string;
|
||||
description: string;
|
||||
}[] = [
|
||||
{
|
||||
value: SchedulingType.COLLECTIVE,
|
||||
label: t("collective"),
|
||||
description: t("collective_description"),
|
||||
},
|
||||
{
|
||||
value: SchedulingType.ROUND_ROBIN,
|
||||
label: t("round_robin"),
|
||||
description: t("round_robin_description"),
|
||||
},
|
||||
];
|
||||
|
||||
const mapUserToValue = ({
|
||||
id,
|
||||
name,
|
||||
username,
|
||||
}: {
|
||||
id: number | null;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
}) => ({
|
||||
value: `${id || ""}`,
|
||||
label: `${name || ""}`,
|
||||
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
||||
});
|
||||
|
||||
const locationFormSchema = z.object({
|
||||
locationType: z.string(),
|
||||
locationAddress: z.string().optional(),
|
||||
@@ -182,7 +135,7 @@ export const EventSetupTab = (
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<li key={location.type} className="mb-2 rounded-sm border border-neutral-300 py-1.5 px-2">
|
||||
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 py-1.5 px-2">
|
||||
<div className="flex justify-between">
|
||||
<div key={index} className="flex flex-grow items-center">
|
||||
<img
|
||||
|
||||
@@ -16,6 +16,7 @@ import { Dialog } from "@calcom/ui/Dialog";
|
||||
import EmptyScreen from "@calcom/ui/EmptyScreen";
|
||||
import { Button, Tooltip, Switch, showToast } from "@calcom/ui/v2";
|
||||
import Dropdown, {
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
@@ -293,7 +294,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
type.$disabled && "pointer-events-none cursor-not-allowed"
|
||||
)}>
|
||||
<Tooltip content={t("show_eventtype_on_profile") as string}>
|
||||
<div className="self-center border-r-2 border-gray-300 pr-2">
|
||||
<div className="self-center border-r-2 border-gray-300 pr-4">
|
||||
<Switch
|
||||
name="Hidden"
|
||||
checked={!type.hidden}
|
||||
@@ -334,31 +335,27 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<Button
|
||||
<DropdownItem
|
||||
type="button"
|
||||
href={"/event-types/" + type.id}
|
||||
color="minimal"
|
||||
disabled={type.$disabled}
|
||||
StartIcon={Icon.FiEdit2}>
|
||||
{t("edit") as string}
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<Button
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="minimal"
|
||||
className={classNames("w-full rounded-none")}
|
||||
data-testid={"event-type-duplicate-" + type.id}
|
||||
disabled={type.$disabled}
|
||||
StartIcon={Icon.FiCopy}
|
||||
onClick={() => openModal(group, type)}>
|
||||
{t("duplicate") as string}
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<EmbedButton
|
||||
color="minimal"
|
||||
size="sm"
|
||||
as={DropdownItem}
|
||||
type="button"
|
||||
StartIcon={Icon.FiCode}
|
||||
className={classNames(
|
||||
@@ -373,17 +370,16 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
|
||||
<DropdownMenuItem>
|
||||
<Button
|
||||
<DropdownItem
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
setDeleteDialogTypeId(type.id);
|
||||
}}
|
||||
color="destructive"
|
||||
StartIcon={Icon.FiTrash}
|
||||
disabled={type.$disabled}
|
||||
className="w-full rounded-none">
|
||||
{t("delete") as string}
|
||||
</Button>
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
|
||||
@@ -8,7 +8,7 @@ export const badgeClassNameByVariant = {
|
||||
orange: "bg-orange-100 text-orange-800",
|
||||
success: "bg-green-100 text-green-800",
|
||||
green: "bg-green-100 text-green-800",
|
||||
gray: "bg-gray-200 text-gray-800 dark:bg-transparent dark:text-darkgray-800",
|
||||
gray: "bg-gray-100 text-gray-800 dark:bg-transparent dark:text-darkgray-800",
|
||||
blue: "bg-blue-100 text-blue-800",
|
||||
red: "bg-red-100 text-red-800",
|
||||
error: "bg-red-100 text-red-800",
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { CheckCircleIcon } from "@heroicons/react/outline";
|
||||
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
||||
import Link from "next/link";
|
||||
import { ComponentProps, forwardRef } from "react";
|
||||
import { Icon } from "react-feather";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
export const Dropdown = DropdownMenuPrimitive.Root;
|
||||
|
||||
@@ -24,10 +28,11 @@ export const DropdownMenuTriggerItem = DropdownMenuPrimitive.TriggerItem;
|
||||
|
||||
type DropdownMenuContentProps = ComponentProps<typeof DropdownMenuPrimitive["Content"]>;
|
||||
export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuContentProps>(
|
||||
({ children, ...props }, forwardedRef) => {
|
||||
({ children, align = "end", ...props }, forwardedRef) => {
|
||||
return (
|
||||
<DropdownMenuPrimitive.Content
|
||||
portalled={props.portalled}
|
||||
align={align}
|
||||
{...props}
|
||||
className="w-50 relative z-10 mt-1 -ml-0 origin-top-right bg-white text-sm ring-1 ring-black ring-opacity-5 focus:outline-none"
|
||||
ref={forwardedRef}>
|
||||
@@ -89,6 +94,48 @@ export const DropdownMenuRadioItem = forwardRef<HTMLDivElement, DropdownMenuRadi
|
||||
);
|
||||
DropdownMenuRadioItem.displayName = "DropdownMenuRadioItem";
|
||||
|
||||
type DropdownItemProps = {
|
||||
children: React.ReactNode;
|
||||
color?: "destructive";
|
||||
StartIcon?: Icon;
|
||||
EndIcon?: Icon;
|
||||
href?: string;
|
||||
disabled?: boolean;
|
||||
} & ButtonOrLinkProps;
|
||||
|
||||
type ButtonOrLinkProps = ComponentProps<"button"> & ComponentProps<"a">;
|
||||
export function ButtonOrLink({ href, ...props }: ButtonOrLinkProps) {
|
||||
const isLink = typeof href !== "undefined";
|
||||
const ButtonOrLink = isLink ? "a" : "button";
|
||||
|
||||
const content = <ButtonOrLink {...props} />;
|
||||
|
||||
if (isLink) {
|
||||
return <Link href={href}>{content}</Link>;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
export const DropdownItem = (props: DropdownItemProps) => {
|
||||
const { StartIcon, EndIcon } = props;
|
||||
|
||||
return (
|
||||
<ButtonOrLink
|
||||
{...props}
|
||||
className={classNames(
|
||||
"inline-flex items-center px-3 py-[10px] text-gray-600",
|
||||
props.color === "destructive" ? "hover:bg-red-100 hover:text-red-700" : " hover:bg-gray-100"
|
||||
)}>
|
||||
<>
|
||||
{StartIcon && <StartIcon />}
|
||||
<div className="mx-2 text-black">{props.children}</div>
|
||||
{EndIcon && <EndIcon />}
|
||||
</>
|
||||
</ButtonOrLink>
|
||||
);
|
||||
};
|
||||
|
||||
export const DropdownMenuSeparator = DropdownMenuPrimitive.Separator;
|
||||
|
||||
export default Dropdown;
|
||||
|
||||
Reference in New Issue
Block a user