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:
sean-brydon
2022-09-02 13:10:55 +02:00
committed by GitHub
parent d27b7ab2c4
commit 03a9996c84
6 changed files with 175 additions and 192 deletions
@@ -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
+9 -13
View File
@@ -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>
+1 -1
View File
@@ -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",
+48 -1
View File
@@ -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;