Added "New Event Type" button on Team settings (#1411)
- Moved CreateNewEventButton in pages/event-types/index to dedicated component as this is used in two places now. - Implemented CreateEventType button on Team settings screen and replaced old markup in on event types page with new component. - Upgrade vanilla JS inputs to library primitives. - Created TextArea & TextAreaField components in components/form. - [Bugfix] Changed back button behavior in Shell to have a specified back path as CreateEventType's modal interfered with the router.goBack behavior. - Ensure modal data is preserved in URL params for router accuracy and removed on exit.
This commit is contained in:
parent
59d4d92b52
commit
70683a89b9
8 changed files with 334 additions and 252 deletions
|
@ -124,7 +124,7 @@ export default function Shell(props: {
|
|||
children: ReactNode;
|
||||
CTA?: ReactNode;
|
||||
HeadingLeftIcon?: ReactNode;
|
||||
showBackButton?: boolean;
|
||||
backPath?: string; // renders back button to specified path
|
||||
// use when content needs to expand with flex
|
||||
flexChildrenContainer?: boolean;
|
||||
}) {
|
||||
|
@ -289,9 +289,12 @@ export default function Shell(props: {
|
|||
props.flexChildrenContainer && "flex flex-col flex-1",
|
||||
"py-8"
|
||||
)}>
|
||||
{props.showBackButton && (
|
||||
{!!props.backPath && (
|
||||
<div className="mx-3 mb-8 sm:mx-8">
|
||||
<Button onClick={() => router.back()} StartIcon={ArrowLeftIcon} color="secondary">
|
||||
<Button
|
||||
onClick={() => router.push(props.backPath as string)}
|
||||
StartIcon={ArrowLeftIcon}
|
||||
color="secondary">
|
||||
Back
|
||||
</Button>
|
||||
</div>
|
||||
|
|
242
components/eventtype/CreateEventType.tsx
Normal file
242
components/eventtype/CreateEventType.tsx
Normal file
|
@ -0,0 +1,242 @@
|
|||
import { ChevronDownIcon, PlusIcon } from "@heroicons/react/solid";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useMutation } from "react-query";
|
||||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
import createEventType from "@lib/mutations/event-types/create-event-type";
|
||||
import showToast from "@lib/notification";
|
||||
import { CreateEventType } from "@lib/types/event-type";
|
||||
|
||||
import { Dialog, DialogClose, DialogContent } from "@components/Dialog";
|
||||
import { TextField, InputLeading, TextAreaField, Form } from "@components/form/fields";
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import { Button } from "@components/ui/Button";
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/ui/Dropdown";
|
||||
import * as RadioArea from "@components/ui/form/radio-area";
|
||||
|
||||
// this describes the uniform data needed to create a new event type on Profile or Team
|
||||
interface EventTypeParent {
|
||||
teamId: number | null | undefined; // if undefined, then it's a profile
|
||||
name?: string | null;
|
||||
slug?: string | null;
|
||||
image?: string | null;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
// set true for use on the team settings page
|
||||
canAddEvents: boolean;
|
||||
// set true when in use on the team settings page
|
||||
isIndividualTeam?: boolean;
|
||||
// EventTypeParent can be a profile (as first option) or a team for the rest.
|
||||
options: EventTypeParent[];
|
||||
}
|
||||
|
||||
export default function CreateEventTypeButton(props: Props) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const modalOpen = useToggleQuery("new");
|
||||
|
||||
const form = useForm<CreateEventType>({
|
||||
defaultValues: { length: 15 },
|
||||
});
|
||||
const { setValue, watch, register } = form;
|
||||
|
||||
useEffect(() => {
|
||||
const subscription = watch((value, { name, type }) => {
|
||||
if (name === "title" && type === "change") {
|
||||
if (value.title) setValue("slug", value.title.replace(/\s+/g, "-").toLowerCase());
|
||||
else setValue("slug", "");
|
||||
}
|
||||
});
|
||||
return () => subscription.unsubscribe();
|
||||
}, [watch, setValue]);
|
||||
|
||||
// URL encoded params
|
||||
const teamId: number | null = Number(router.query.teamId) || null;
|
||||
const pageSlug = router.query.eventPage || props.options[0].slug;
|
||||
|
||||
const hasTeams = !!props.options.find((option) => option.teamId);
|
||||
|
||||
const createMutation = useMutation(createEventType, {
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.push("/event-types/" + eventType.id);
|
||||
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
onError: (err: HttpError) => {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
// inject selection data into url for correct router history
|
||||
const openModal = (option: EventTypeParent) => {
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
new: "1",
|
||||
eventPage: option.slug,
|
||||
...(option.teamId
|
||||
? {
|
||||
teamId: option.teamId,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// remove url params after close modal to reset state
|
||||
const closeModal = () => {
|
||||
router.replace({
|
||||
pathname: router.pathname,
|
||||
query: { id: router.query.id },
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={modalOpen.isOn}
|
||||
onOpenChange={(isOpen) => {
|
||||
router.push(isOpen ? modalOpen.hrefOn : modalOpen.hrefOff);
|
||||
if (!isOpen) closeModal();
|
||||
}}>
|
||||
{!hasTeams || props.isIndividualTeam ? (
|
||||
<Button
|
||||
onClick={() => openModal(props.options[0])}
|
||||
data-testid="new-event-type"
|
||||
StartIcon={PlusIcon}
|
||||
{...(props.canAddEvents
|
||||
? {
|
||||
href: modalOpen.hrefOn,
|
||||
}
|
||||
: {
|
||||
disabled: true,
|
||||
})}>
|
||||
{t("new_event_type_btn")}
|
||||
</Button>
|
||||
) : (
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button EndIcon={ChevronDownIcon}>{t("new_event_type_btn")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
{props.options.map((option) => (
|
||||
<DropdownMenuItem
|
||||
key={option.slug}
|
||||
className="px-3 py-2 cursor-pointer hover:bg-neutral-100 focus:outline-none"
|
||||
onSelect={() => openModal(option)}>
|
||||
<Avatar alt={option.name || ""} imageSrc={option.image} size={6} className="inline mr-2" />
|
||||
{option.name ? option.name : option.slug}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
<DialogContent>
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
||||
</h3>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
const payload: CreateEventType = {
|
||||
title: values.title,
|
||||
slug: values.slug,
|
||||
description: values.description,
|
||||
length: values.length,
|
||||
};
|
||||
if (router.query.teamId) {
|
||||
payload.teamId = parseInt(`${router.query.teamId}`, 10);
|
||||
payload.schedulingType = values.schedulingType as SchedulingType;
|
||||
}
|
||||
|
||||
createMutation.mutate(payload);
|
||||
}}>
|
||||
<div className="mt-3 space-y-4">
|
||||
<TextField label={t("title")} placeholder={t("quick_chat")} {...register("title")} />
|
||||
|
||||
<TextField
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<InputLeading>
|
||||
{process.env.NEXT_PUBLIC_APP_URL}/{pageSlug}/
|
||||
</InputLeading>
|
||||
}
|
||||
{...register("slug")}
|
||||
/>
|
||||
|
||||
<TextAreaField
|
||||
label={t("description")}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
{...register("description")}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<TextField
|
||||
type="number"
|
||||
required
|
||||
placeholder="15"
|
||||
defaultValue={15}
|
||||
label={t("length")}
|
||||
className="pr-20"
|
||||
{...register("length")}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pt-4 mt-1.5 pr-3 text-sm text-gray-400">
|
||||
{t("minutes")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{teamId && (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="schedulingType" className="block text-sm font-bold text-gray-700">
|
||||
{t("scheduling_type")}
|
||||
</label>
|
||||
<RadioArea.Group
|
||||
{...register("schedulingType")}
|
||||
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
|
||||
className="relative flex mt-1 space-x-6 rounded-sm shadow-sm">
|
||||
<RadioArea.Item value={SchedulingType.COLLECTIVE} className="w-1/2 text-sm">
|
||||
<strong className="block mb-1">{t("collective")}</strong>
|
||||
<p>{t("collective_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item value={SchedulingType.ROUND_ROBIN} className="w-1/2 text-sm">
|
||||
<strong className="block mb-1">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row-reverse mt-8 gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button color="secondary">{t("cancel")}</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -10,6 +10,7 @@ import showToast from "@lib/notification";
|
|||
import { Alert } from "@components/ui/Alert";
|
||||
|
||||
type InputProps = Omit<JSX.IntrinsicElements["input"], "name"> & { name: string };
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
|
||||
return (
|
||||
<input
|
||||
|
@ -31,6 +32,14 @@ export function Label(props: JSX.IntrinsicElements["label"]) {
|
|||
);
|
||||
}
|
||||
|
||||
export function InputLeading(props: JSX.IntrinsicElements["div"]) {
|
||||
return (
|
||||
<span className="inline-flex items-center flex-shrink-0 px-3 text-gray-500 border border-r-0 border-gray-300 rounded-l-sm bg-gray-50 sm:text-sm">
|
||||
{props.children}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
type InputFieldProps = {
|
||||
label?: ReactNode;
|
||||
addOnLeading?: ReactNode;
|
||||
|
@ -50,7 +59,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
|
|||
: "",
|
||||
className,
|
||||
addOnLeading,
|
||||
...passThroughToInput
|
||||
...passThrough
|
||||
} = props;
|
||||
return (
|
||||
<div>
|
||||
|
@ -65,13 +74,13 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
|
|||
<Input
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
className={classNames(className, "mt-0")}
|
||||
{...passThroughToInput}
|
||||
className={classNames(className, "mt-0", props.addOnLeading && "rounded-l-none")}
|
||||
{...passThrough}
|
||||
ref={ref}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<Input id={id} placeholder={placeholder} className={className} {...passThroughToInput} ref={ref} />
|
||||
<Input id={id} placeholder={placeholder} className={className} {...passThrough} ref={ref} />
|
||||
)}
|
||||
{methods?.formState?.errors[props.name] && (
|
||||
<Alert className="mt-1" severity="error" message={methods.formState.errors[props.name].message} />
|
||||
|
@ -112,6 +121,57 @@ export const EmailField = forwardRef<HTMLInputElement, InputFieldProps>(function
|
|||
return <EmailInput ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
type TextAreaProps = Omit<JSX.IntrinsicElements["textarea"], "name"> & { name: string };
|
||||
|
||||
export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function TextAreaInput(props, ref) {
|
||||
return (
|
||||
<textarea
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={classNames(
|
||||
"block w-full font-mono border-gray-300 rounded-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm",
|
||||
props.className
|
||||
)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
type TextAreaFieldProps = {
|
||||
label?: ReactNode;
|
||||
} & React.ComponentProps<typeof TextArea> & {
|
||||
labelProps?: React.ComponentProps<typeof Label>;
|
||||
};
|
||||
|
||||
export const TextAreaField = forwardRef<HTMLTextAreaElement, TextAreaFieldProps>(function TextField(
|
||||
props,
|
||||
ref
|
||||
) {
|
||||
const id = useId();
|
||||
const { t } = useLocale();
|
||||
const methods = useFormContext();
|
||||
const {
|
||||
label = t(props.name as string),
|
||||
labelProps,
|
||||
placeholder = t(props.name + "_placeholder") !== props.name + "_placeholder"
|
||||
? t(props.name + "_placeholder")
|
||||
: "",
|
||||
...passThrough
|
||||
} = props;
|
||||
return (
|
||||
<div>
|
||||
{!!props.name && (
|
||||
<Label htmlFor={id} {...labelProps}>
|
||||
{label}
|
||||
</Label>
|
||||
)}
|
||||
<TextArea ref={ref} placeholder={placeholder} {...passThrough} />
|
||||
{methods?.formState?.errors[props.name] && (
|
||||
<Alert className="mt-1" severity="error" message={methods.formState.errors[props.name].message} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
type FormProps<T> = { form: UseFormReturn<T>; handleSubmit: SubmitHandler<T> } & Omit<
|
||||
JSX.IntrinsicElements["form"],
|
||||
"onSubmit"
|
||||
|
|
|
@ -10,6 +10,7 @@ import { trpc } from "@lib/trpc";
|
|||
|
||||
import { Dialog, DialogTrigger } from "@components/Dialog";
|
||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
|
||||
import LinkIconButton from "@components/ui/LinkIconButton";
|
||||
|
||||
import { MembershipRole } from ".prisma/client";
|
||||
|
@ -49,6 +50,13 @@ export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers;
|
|||
|
||||
return (
|
||||
<div className="px-2 space-y-6">
|
||||
<CreateEventTypeButton
|
||||
isIndividualTeam
|
||||
canAddEvents={true}
|
||||
options={[
|
||||
{ teamId: props.team?.id, name: props.team?.name, slug: props.team?.slug, image: props.team?.logo },
|
||||
]}
|
||||
/>
|
||||
{/* <Switch
|
||||
name="isHidden"
|
||||
defaultChecked={hidden}
|
||||
|
|
|
@ -33,7 +33,7 @@ export function TeamAvailabilityPage() {
|
|||
|
||||
return (
|
||||
<Shell
|
||||
showBackButton={!errorMessage}
|
||||
backPath={!errorMessage ? `/settings/teams/${team?.id}` : undefined}
|
||||
heading={!isFreeUser && team?.name}
|
||||
flexChildrenContainer
|
||||
subtitle={team && !isFreeUser && "Your team's availability at a glance"}
|
||||
|
|
|
@ -1,49 +1,31 @@
|
|||
// TODO: replace headlessui with radix-ui
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import {
|
||||
ArrowDownIcon,
|
||||
ArrowUpIcon,
|
||||
DotsHorizontalIcon,
|
||||
ExternalLinkIcon,
|
||||
LinkIcon,
|
||||
ArrowDownIcon,
|
||||
ChevronDownIcon,
|
||||
PlusIcon,
|
||||
ArrowUpIcon,
|
||||
UsersIcon,
|
||||
} from "@heroicons/react/solid";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { Fragment, useRef, useState, useEffect } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import React, { Fragment, useEffect, useState } from "react";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
import classNames from "@lib/classNames";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
import createEventType from "@lib/mutations/event-types/create-event-type";
|
||||
import showToast from "@lib/notification";
|
||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
||||
import { CreateEventType } from "@lib/types/event-type";
|
||||
|
||||
import { Dialog, DialogClose, DialogContent } from "@components/Dialog";
|
||||
import Shell from "@components/Shell";
|
||||
import { Tooltip } from "@components/Tooltip";
|
||||
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
|
||||
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
||||
import { Alert } from "@components/ui/Alert";
|
||||
import Avatar from "@components/ui/Avatar";
|
||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||
import Badge from "@components/ui/Badge";
|
||||
import { Button } from "@components/ui/Button";
|
||||
import Dropdown, {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@components/ui/Dropdown";
|
||||
import * as RadioArea from "@components/ui/form/radio-area";
|
||||
import UserCalendarIllustration from "@components/ui/svg/UserCalendarIllustration";
|
||||
|
||||
type Profiles = inferQueryOutput<"viewer.eventTypes">["profiles"];
|
||||
|
@ -64,7 +46,7 @@ const CreateFirstEventTypeView = ({ canAddEvents, profiles }: CreateEventTypePro
|
|||
<div className="block mx-auto text-center md:max-w-screen-sm">
|
||||
<h3 className="mt-2 text-xl font-bold text-neutral-900">{t("new_event_type_heading")}</h3>
|
||||
<p className="mt-1 mb-2 text-md text-neutral-600">{t("new_event_type_description")}</p>
|
||||
<CreateNewEventButton canAddEvents={canAddEvents} profiles={profiles} />
|
||||
<CreateEventTypeButton canAddEvents={canAddEvents} options={profiles} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -332,9 +314,9 @@ const EventTypesPage = () => {
|
|||
CTA={
|
||||
query.data &&
|
||||
query.data.eventTypeGroups.length !== 0 && (
|
||||
<CreateNewEventButton
|
||||
<CreateEventTypeButton
|
||||
canAddEvents={query.data.viewer.canAddEvents}
|
||||
profiles={query.data.profiles}
|
||||
options={query.data.profiles}
|
||||
/>
|
||||
)
|
||||
}>
|
||||
|
@ -385,217 +367,4 @@ const EventTypesPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
const CreateNewEventButton = ({ profiles, canAddEvents }: CreateEventTypeProps) => {
|
||||
const router = useRouter();
|
||||
const teamId: number | null = Number(router.query.teamId) || null;
|
||||
const modalOpen = useToggleQuery("new");
|
||||
const { t } = useLocale();
|
||||
|
||||
const createMutation = useMutation(createEventType, {
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.push("/event-types/" + eventType.id);
|
||||
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
onError: (err: HttpError) => {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const slugRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={modalOpen.isOn}
|
||||
onOpenChange={(isOpen) => {
|
||||
router.push(isOpen ? modalOpen.hrefOn : modalOpen.hrefOff);
|
||||
}}>
|
||||
{!profiles.filter((profile) => profile.teamId).length && (
|
||||
<Button
|
||||
data-testid="new-event-type"
|
||||
{...(canAddEvents
|
||||
? {
|
||||
href: modalOpen.hrefOn,
|
||||
}
|
||||
: {
|
||||
disabled: true,
|
||||
})}
|
||||
StartIcon={PlusIcon}>
|
||||
{t("new_event_type_btn")}
|
||||
</Button>
|
||||
)}
|
||||
{profiles.filter((profile) => profile.teamId).length > 0 && (
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button EndIcon={ChevronDownIcon}>{t("new_event_type_btn")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{t("new_event_subtitle")}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
{profiles.map((profile) => (
|
||||
<DropdownMenuItem
|
||||
key={profile.slug}
|
||||
className="px-3 py-2 cursor-pointer hover:bg-neutral-100 focus:outline-none"
|
||||
onSelect={() =>
|
||||
router.push({
|
||||
pathname: router.pathname,
|
||||
query: {
|
||||
...router.query,
|
||||
new: "1",
|
||||
eventPage: profile.slug,
|
||||
...(profile.teamId
|
||||
? {
|
||||
teamId: profile.teamId,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
})
|
||||
}>
|
||||
<Avatar alt={profile.name || ""} imageSrc={profile.image} size={6} className="inline mr-2" />
|
||||
{profile.name ? profile.name : profile.slug}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
)}
|
||||
<DialogContent>
|
||||
<div className="mb-8">
|
||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||
{teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
||||
</h3>
|
||||
<div>
|
||||
<p className="text-sm text-gray-500">{t("new_event_type_to_book_description")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const target = e.target as unknown as Record<
|
||||
"title" | "slug" | "description" | "length" | "schedulingType",
|
||||
{ value: string }
|
||||
>;
|
||||
|
||||
const payload: CreateEventType = {
|
||||
title: target.title.value,
|
||||
slug: target.slug.value,
|
||||
description: target.description.value,
|
||||
length: parseInt(target.length.value),
|
||||
};
|
||||
|
||||
if (router.query.teamId) {
|
||||
payload.teamId = parseInt(`${router.query.teamId}`, 10);
|
||||
payload.schedulingType = target.schedulingType.value as SchedulingType;
|
||||
}
|
||||
|
||||
createMutation.mutate(payload);
|
||||
}}>
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="title" className="block text-sm font-medium text-gray-700">
|
||||
{t("title")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<input
|
||||
onChange={(e) => {
|
||||
if (!slugRef.current) {
|
||||
return;
|
||||
}
|
||||
slugRef.current.value = e.target.value.replace(/\s+/g, "-").toLowerCase();
|
||||
}}
|
||||
type="text"
|
||||
name="title"
|
||||
id="title"
|
||||
required
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
||||
placeholder={t("quick_chat")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="slug" className="block text-sm font-medium text-gray-700">
|
||||
{t("url")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<div className="flex rounded-sm shadow-sm">
|
||||
<span className="inline-flex items-center px-3 text-gray-500 border border-r-0 border-gray-300 rounded-l-md bg-gray-50 sm:text-sm">
|
||||
{process.env.NEXT_PUBLIC_APP_URL}/{router.query.eventPage || profiles[0].slug}/
|
||||
</span>
|
||||
<input
|
||||
ref={slugRef}
|
||||
type="text"
|
||||
name="slug"
|
||||
id="slug"
|
||||
required
|
||||
className="flex-1 block w-full min-w-0 border-gray-300 rounded-none focus:ring-neutral-900 focus:border-neutral-900 rounded-r-md sm:text-sm"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="description" className="block text-sm font-medium text-gray-700">
|
||||
{t("description")}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
<textarea
|
||||
name="description"
|
||||
id="description"
|
||||
className="block w-full border-gray-300 rounded-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
||||
placeholder={t("quick_video_meeting")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="length" className="block text-sm font-medium text-gray-700">
|
||||
{t("length")}
|
||||
</label>
|
||||
<div className="relative mt-1 rounded-sm shadow-sm">
|
||||
<input
|
||||
type="number"
|
||||
name="length"
|
||||
id="length"
|
||||
required
|
||||
className="block w-full pr-20 border-gray-300 rounded-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
||||
placeholder="15"
|
||||
defaultValue={15}
|
||||
/>
|
||||
<div className="absolute inset-y-0 right-0 flex items-center pr-3 text-sm text-gray-400">
|
||||
{t("minutes")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{teamId && (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="schedulingType" className="block text-sm font-medium text-gray-700">
|
||||
{t("scheduling_type")}
|
||||
</label>
|
||||
<RadioArea.Group
|
||||
name="schedulingType"
|
||||
className="relative flex mt-1 space-x-6 rounded-sm shadow-sm">
|
||||
<RadioArea.Item value={SchedulingType.COLLECTIVE} className="w-1/2 text-sm">
|
||||
<strong className="block mb-1">{t("collective")}</strong>
|
||||
<p>{t("collective_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item value={SchedulingType.ROUND_ROBIN} className="w-1/2 text-sm">
|
||||
<strong className="block mb-1">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-row-reverse mt-8 gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<DialogClose asChild>
|
||||
<Button color="secondary">{t("cancel")}</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EventTypesPage;
|
||||
|
|
|
@ -19,7 +19,7 @@ import Loader from "@components/Loader";
|
|||
import Shell, { ShellSubHeading } from "@components/Shell";
|
||||
import { Tooltip } from "@components/Tooltip";
|
||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||
import { FieldsetLegend, Form, InputGroupBox, TextField } from "@components/form/fields";
|
||||
import { FieldsetLegend, Form, InputGroupBox, TextField, TextArea } from "@components/form/fields";
|
||||
import { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||
import ConnectIntegration from "@components/integrations/ConnectIntegrations";
|
||||
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||
|
@ -270,11 +270,11 @@ function WebhookDialogForm(props: {
|
|||
</label>
|
||||
</div>
|
||||
{useCustomPayloadTemplate && (
|
||||
<textarea
|
||||
<TextArea
|
||||
{...form.register("payloadTemplate")}
|
||||
className="block w-full font-mono border-gray-300 rounded-sm shadow-sm focus:ring-neutral-900 focus:border-neutral-900 sm:text-sm"
|
||||
rows={5}
|
||||
defaultValue={useCustomPayloadTemplate && (defaultValues.payloadTemplate || "")}></textarea>
|
||||
defaultValue={useCustomPayloadTemplate && (defaultValues.payloadTemplate || "")}
|
||||
rows={3}
|
||||
/>
|
||||
)}
|
||||
</fieldset>
|
||||
<WebhookTestDisclosure />
|
||||
|
|
|
@ -33,7 +33,7 @@ export function TeamSettingsPage() {
|
|||
|
||||
return (
|
||||
<Shell
|
||||
showBackButton={!errorMessage}
|
||||
backPath={!errorMessage ? `/settings/teams` : undefined}
|
||||
heading={team?.name}
|
||||
subtitle={team && "Manage this team"}
|
||||
HeadingLeftIcon={
|
||||
|
|
Loading…
Reference in a new issue