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;
|
children: ReactNode;
|
||||||
CTA?: ReactNode;
|
CTA?: ReactNode;
|
||||||
HeadingLeftIcon?: ReactNode;
|
HeadingLeftIcon?: ReactNode;
|
||||||
showBackButton?: boolean;
|
backPath?: string; // renders back button to specified path
|
||||||
// use when content needs to expand with flex
|
// use when content needs to expand with flex
|
||||||
flexChildrenContainer?: boolean;
|
flexChildrenContainer?: boolean;
|
||||||
}) {
|
}) {
|
||||||
|
@ -289,9 +289,12 @@ export default function Shell(props: {
|
||||||
props.flexChildrenContainer && "flex flex-col flex-1",
|
props.flexChildrenContainer && "flex flex-col flex-1",
|
||||||
"py-8"
|
"py-8"
|
||||||
)}>
|
)}>
|
||||||
{props.showBackButton && (
|
{!!props.backPath && (
|
||||||
<div className="mx-3 mb-8 sm:mx-8">
|
<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
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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";
|
import { Alert } from "@components/ui/Alert";
|
||||||
|
|
||||||
type InputProps = Omit<JSX.IntrinsicElements["input"], "name"> & { name: string };
|
type InputProps = Omit<JSX.IntrinsicElements["input"], "name"> & { name: string };
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
|
export const Input = forwardRef<HTMLInputElement, InputProps>(function Input(props, ref) {
|
||||||
return (
|
return (
|
||||||
<input
|
<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 = {
|
type InputFieldProps = {
|
||||||
label?: ReactNode;
|
label?: ReactNode;
|
||||||
addOnLeading?: ReactNode;
|
addOnLeading?: ReactNode;
|
||||||
|
@ -50,7 +59,7 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
|
||||||
: "",
|
: "",
|
||||||
className,
|
className,
|
||||||
addOnLeading,
|
addOnLeading,
|
||||||
...passThroughToInput
|
...passThrough
|
||||||
} = props;
|
} = props;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -65,13 +74,13 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
|
||||||
<Input
|
<Input
|
||||||
id={id}
|
id={id}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
className={classNames(className, "mt-0")}
|
className={classNames(className, "mt-0", props.addOnLeading && "rounded-l-none")}
|
||||||
{...passThroughToInput}
|
{...passThrough}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
/>
|
/>
|
||||||
</div>
|
</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] && (
|
{methods?.formState?.errors[props.name] && (
|
||||||
<Alert className="mt-1" severity="error" message={methods.formState.errors[props.name].message} />
|
<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} />;
|
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<
|
type FormProps<T> = { form: UseFormReturn<T>; handleSubmit: SubmitHandler<T> } & Omit<
|
||||||
JSX.IntrinsicElements["form"],
|
JSX.IntrinsicElements["form"],
|
||||||
"onSubmit"
|
"onSubmit"
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { trpc } from "@lib/trpc";
|
||||||
|
|
||||||
import { Dialog, DialogTrigger } from "@components/Dialog";
|
import { Dialog, DialogTrigger } from "@components/Dialog";
|
||||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
||||||
|
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
|
||||||
import LinkIconButton from "@components/ui/LinkIconButton";
|
import LinkIconButton from "@components/ui/LinkIconButton";
|
||||||
|
|
||||||
import { MembershipRole } from ".prisma/client";
|
import { MembershipRole } from ".prisma/client";
|
||||||
|
@ -49,6 +50,13 @@ export default function TeamSettingsRightSidebar(props: { team: TeamWithMembers;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-2 space-y-6">
|
<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
|
{/* <Switch
|
||||||
name="isHidden"
|
name="isHidden"
|
||||||
defaultChecked={hidden}
|
defaultChecked={hidden}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export function TeamAvailabilityPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
showBackButton={!errorMessage}
|
backPath={!errorMessage ? `/settings/teams/${team?.id}` : undefined}
|
||||||
heading={!isFreeUser && team?.name}
|
heading={!isFreeUser && team?.name}
|
||||||
flexChildrenContainer
|
flexChildrenContainer
|
||||||
subtitle={team && !isFreeUser && "Your team's availability at a glance"}
|
subtitle={team && !isFreeUser && "Your team's availability at a glance"}
|
||||||
|
|
|
@ -1,49 +1,31 @@
|
||||||
// TODO: replace headlessui with radix-ui
|
// TODO: replace headlessui with radix-ui
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import {
|
import {
|
||||||
|
ArrowDownIcon,
|
||||||
|
ArrowUpIcon,
|
||||||
DotsHorizontalIcon,
|
DotsHorizontalIcon,
|
||||||
ExternalLinkIcon,
|
ExternalLinkIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
ArrowDownIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
PlusIcon,
|
|
||||||
ArrowUpIcon,
|
|
||||||
UsersIcon,
|
UsersIcon,
|
||||||
} from "@heroicons/react/solid";
|
} from "@heroicons/react/solid";
|
||||||
import { SchedulingType } from "@prisma/client";
|
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import React, { Fragment, useEffect, useState } from "react";
|
||||||
import React, { Fragment, useRef, useState, useEffect } from "react";
|
|
||||||
import { useMutation } from "react-query";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
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 showToast from "@lib/notification";
|
||||||
import { inferQueryOutput, trpc } from "@lib/trpc";
|
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 Shell from "@components/Shell";
|
||||||
import { Tooltip } from "@components/Tooltip";
|
import { Tooltip } from "@components/Tooltip";
|
||||||
|
import CreateEventTypeButton from "@components/eventtype/CreateEventType";
|
||||||
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
import EventTypeDescription from "@components/eventtype/EventTypeDescription";
|
||||||
import { Alert } from "@components/ui/Alert";
|
import { Alert } from "@components/ui/Alert";
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||||
import Badge from "@components/ui/Badge";
|
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";
|
import UserCalendarIllustration from "@components/ui/svg/UserCalendarIllustration";
|
||||||
|
|
||||||
type Profiles = inferQueryOutput<"viewer.eventTypes">["profiles"];
|
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">
|
<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>
|
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -332,9 +314,9 @@ const EventTypesPage = () => {
|
||||||
CTA={
|
CTA={
|
||||||
query.data &&
|
query.data &&
|
||||||
query.data.eventTypeGroups.length !== 0 && (
|
query.data.eventTypeGroups.length !== 0 && (
|
||||||
<CreateNewEventButton
|
<CreateEventTypeButton
|
||||||
canAddEvents={query.data.viewer.canAddEvents}
|
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;
|
export default EventTypesPage;
|
||||||
|
|
|
@ -19,7 +19,7 @@ import Loader from "@components/Loader";
|
||||||
import Shell, { ShellSubHeading } from "@components/Shell";
|
import Shell, { ShellSubHeading } from "@components/Shell";
|
||||||
import { Tooltip } from "@components/Tooltip";
|
import { Tooltip } from "@components/Tooltip";
|
||||||
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
|
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 { CalendarListContainer } from "@components/integrations/CalendarListContainer";
|
||||||
import ConnectIntegration from "@components/integrations/ConnectIntegrations";
|
import ConnectIntegration from "@components/integrations/ConnectIntegrations";
|
||||||
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||||
|
@ -270,11 +270,11 @@ function WebhookDialogForm(props: {
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
{useCustomPayloadTemplate && (
|
{useCustomPayloadTemplate && (
|
||||||
<textarea
|
<TextArea
|
||||||
{...form.register("payloadTemplate")}
|
{...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"
|
defaultValue={useCustomPayloadTemplate && (defaultValues.payloadTemplate || "")}
|
||||||
rows={5}
|
rows={3}
|
||||||
defaultValue={useCustomPayloadTemplate && (defaultValues.payloadTemplate || "")}></textarea>
|
/>
|
||||||
)}
|
)}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<WebhookTestDisclosure />
|
<WebhookTestDisclosure />
|
||||||
|
|
|
@ -33,7 +33,7 @@ export function TeamSettingsPage() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
showBackButton={!errorMessage}
|
backPath={!errorMessage ? `/settings/teams` : undefined}
|
||||||
heading={team?.name}
|
heading={team?.name}
|
||||||
subtitle={team && "Manage this team"}
|
subtitle={team && "Manage this team"}
|
||||||
HeadingLeftIcon={
|
HeadingLeftIcon={
|
||||||
|
|
Loading…
Reference in a new issue