fix/duplicate-event-types (#5888)
* Add new modal for duplicate and trpc action for it * Adding event type duplication validations * Fix type errors and id not being removed from rest obj * Fix test for duplicating event * Fix test for duplicating event * Add connect to event type workflows * fix button for adding new events Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>pull/5976/head
parent
e7ff5508d7
commit
c1e23bfdc6
|
@ -177,22 +177,17 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
}
|
||||
|
||||
// inject selection data into url for correct router history
|
||||
const openModal = (group: EventTypeGroup, type: EventType) => {
|
||||
const openDuplicateModal = (eventType: EventType) => {
|
||||
const query = {
|
||||
...router.query,
|
||||
dialog: "new-eventtype",
|
||||
eventPage: group.profile.slug,
|
||||
title: type.title,
|
||||
slug: type.slug,
|
||||
description: type.description,
|
||||
length: type.length,
|
||||
type: type.schedulingType,
|
||||
teamId: group.teamId,
|
||||
locations: encodeURIComponent(JSON.stringify(type.locations)),
|
||||
dialog: "duplicate-event-type",
|
||||
title: eventType.title,
|
||||
description: eventType.description,
|
||||
slug: eventType.slug,
|
||||
id: eventType.id,
|
||||
length: eventType.length,
|
||||
};
|
||||
if (!group.teamId) {
|
||||
delete query.teamId;
|
||||
}
|
||||
|
||||
router.push(
|
||||
{
|
||||
pathname: router.pathname,
|
||||
|
@ -361,7 +356,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
type="button"
|
||||
data-testid={"event-type-duplicate-" + type.id}
|
||||
StartIcon={Icon.FiCopy}
|
||||
onClick={() => openModal(group, type)}>
|
||||
onClick={() => openDuplicateModal(type)}>
|
||||
{t("duplicate") as string}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
|
@ -469,7 +464,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
className="w-full rounded-none"
|
||||
data-testid={"event-type-duplicate-" + type.id}
|
||||
StartIcon={Icon.FiCopy}
|
||||
onClick={() => openModal(group, type)}>
|
||||
onClick={() => openDuplicateModal(type)}>
|
||||
{t("duplicate") as string}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
@ -8,10 +8,10 @@ import { test } from "./lib/fixtures";
|
|||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
test.describe("Event Types tests", () => {
|
||||
test.describe("pro user", () => {
|
||||
test.describe("user", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
const proUser = await users.create();
|
||||
await proUser.login();
|
||||
const user = await users.create();
|
||||
await user.login();
|
||||
await page.goto("/event-types");
|
||||
// We wait until loading is finished
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
|
@ -101,13 +101,13 @@ test.describe("Event Types tests", () => {
|
|||
const params = new URLSearchParams(url);
|
||||
|
||||
expect(params.get("title")).toBe(firstTitle);
|
||||
expect(params.get("slug")).toBe(firstSlug);
|
||||
expect(params.get("slug")).toContain(firstSlug);
|
||||
|
||||
const formTitle = await page.inputValue("[name=title]");
|
||||
const formSlug = await page.inputValue("[name=slug]");
|
||||
|
||||
expect(formTitle).toBe(firstTitle);
|
||||
expect(formSlug).toBe(firstSlug);
|
||||
expect(formSlug).toContain(firstSlug);
|
||||
});
|
||||
test("edit first event", async ({ page }) => {
|
||||
const $eventTypes = page.locator("[data-testid=event-types] > li a");
|
||||
|
|
|
@ -247,7 +247,7 @@ const createUser = async (
|
|||
opts?: CustomUserOpts | null
|
||||
): Promise<PrismaType.UserCreateInput> => {
|
||||
// build a unique name for our user
|
||||
const uname = `${opts?.username}-${workerInfo.workerIndex}-${Date.now()}`;
|
||||
const uname = `${opts?.username || "user"}-${workerInfo.workerIndex}-${Date.now()}`;
|
||||
return {
|
||||
username: uname,
|
||||
name: opts?.name,
|
||||
|
|
|
@ -750,7 +750,7 @@
|
|||
"toggle_calendars_conflict": "Toggle the calendars you want to check for conflicts to prevent double bookings.",
|
||||
"select_destination_calendar": "Create events on",
|
||||
"connect_additional_calendar": "Connect additional calendar",
|
||||
"calendar_updated_successfully":"Calendar updated successfully",
|
||||
"calendar_updated_successfully": "Calendar updated successfully",
|
||||
"conferencing": "Conferencing",
|
||||
"calendar": "Calendar",
|
||||
"payments": "Payments",
|
||||
|
@ -1389,9 +1389,6 @@
|
|||
"attendee_email_workflow": "Attendee email",
|
||||
"attendee_email_info": "The person booking's email",
|
||||
"kbar_search_placeholder": "Type a command or search...",
|
||||
"free_to_use_apps": "Free",
|
||||
"enterprise_license": "This is an enterprise feature",
|
||||
"enterprise_license_description": "To enable this feature, get a deployment key at {{consoleUrl}} console and add it to your .env as CALCOM_LICENSE_KEY. If your team already has a license, please contact {{supportMail}} for help.",
|
||||
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.",
|
||||
"choose_common_schedule_team_event": "Choose a common schedule",
|
||||
"choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.",
|
||||
|
@ -1412,7 +1409,7 @@
|
|||
"add_one_fixed_attendee": "Add one fixed attendee and round robin through a number of attendees.",
|
||||
"calcom_is_better_with_team": "Cal.com is better with teams",
|
||||
"add_your_team_members": "Add your team members to your event types. Use collective scheduling to include everyone or find the most suitable person with round robin scheduling.",
|
||||
"booking_limit_reached":"Booking Limit for this event type has been reached",
|
||||
"booking_limit_reached": "Booking Limit for this event type has been reached",
|
||||
"admin_has_disabled": "An admin has disabled {{appName}}",
|
||||
"disabled_app_affects_event_type": "An admin has disabled {{appName}} which affects your event type {{eventType}}",
|
||||
"disable_payment_app": "The admin has disabled {{appName}} which affects your event type {{title}}. Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin renables your payment method.",
|
||||
|
@ -1440,6 +1437,6 @@
|
|||
"enter_option": "Enter Option {{index}}",
|
||||
"add_an_option": "Add an option",
|
||||
"radio": "Radio",
|
||||
"kbar_search_placeholder" : "Start typing to search",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
||||
"set_as_default": "Set as default"
|
||||
}
|
||||
|
|
|
@ -34,6 +34,8 @@ import {
|
|||
TextField,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { DuplicateDialog } from "./DuplicateDialog";
|
||||
|
||||
// this describes the uniform data needed to create a new event type on Profile or Team
|
||||
export interface EventTypeParent {
|
||||
teamId: number | null | undefined; // if undefined, then it's a profile
|
||||
|
@ -149,18 +151,7 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
|||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
name="new-eventtype"
|
||||
clearQueryParamsOnClose={[
|
||||
"eventPage",
|
||||
"teamId",
|
||||
"type",
|
||||
"description",
|
||||
"title",
|
||||
"length",
|
||||
"slug",
|
||||
"locations",
|
||||
]}>
|
||||
<>
|
||||
{!hasTeams || props.isIndividualTeam ? (
|
||||
<Button
|
||||
onClick={() => openModal(props.options[0])}
|
||||
|
@ -200,124 +191,140 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
|||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
)}
|
||||
|
||||
<DialogContent
|
||||
type="creation"
|
||||
className="overflow-y-auto"
|
||||
title={teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
||||
description={t("new_event_type_to_book_description")}>
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-6">
|
||||
{teamId && (
|
||||
<TextField
|
||||
type="hidden"
|
||||
labelProps={{ style: { display: "none" } }}
|
||||
{...register("teamId", { valueAsNumber: true })}
|
||||
value={teamId}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
label={t("title")}
|
||||
placeholder={t("quick_chat")}
|
||||
{...register("title")}
|
||||
onChange={(e) => {
|
||||
form.setValue("title", e?.target.value);
|
||||
if (form.formState.touchedFields["slug"] === undefined) {
|
||||
form.setValue("slug", slugify(e?.target.value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
|
||||
<TextField
|
||||
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
|
||||
required
|
||||
addOnLeading={<>/{pageSlug}/</>}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
|
||||
</>
|
||||
}
|
||||
{...register("slug")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextAreaField
|
||||
label={t("description")}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
{...register("description")}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<TextField
|
||||
type="number"
|
||||
required
|
||||
min="10"
|
||||
placeholder="15"
|
||||
label={t("length")}
|
||||
className="pr-20"
|
||||
{...register("length", { valueAsNumber: true })}
|
||||
addOnSuffix={t("minutes")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{teamId && (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="schedulingType" className="block text-sm font-bold text-gray-700">
|
||||
{t("scheduling_type")}
|
||||
</label>
|
||||
{form.formState.errors.schedulingType && (
|
||||
<Alert
|
||||
className="mt-1"
|
||||
severity="error"
|
||||
message={form.formState.errors.schedulingType.message}
|
||||
{/* Dialog for duplicate event type */}
|
||||
{router.query.dialog === "duplicate-event-type" && <DuplicateDialog />}
|
||||
{router.query.dialog === "new-eventtype" && (
|
||||
<Dialog
|
||||
name="new-eventtype"
|
||||
clearQueryParamsOnClose={[
|
||||
"eventPage",
|
||||
"teamId",
|
||||
"type",
|
||||
"description",
|
||||
"title",
|
||||
"length",
|
||||
"slug",
|
||||
"locations",
|
||||
]}>
|
||||
<DialogContent
|
||||
type="creation"
|
||||
className="overflow-y-auto"
|
||||
title={teamId ? t("add_new_team_event_type") : t("add_new_event_type")}
|
||||
description={t("new_event_type_to_book_description")}>
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-6">
|
||||
{teamId && (
|
||||
<TextField
|
||||
type="hidden"
|
||||
labelProps={{ style: { display: "none" } }}
|
||||
{...register("teamId", { valueAsNumber: true })}
|
||||
value={teamId}
|
||||
/>
|
||||
)}
|
||||
<RadioArea.Group
|
||||
{...register("schedulingType")}
|
||||
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
|
||||
className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse">
|
||||
<RadioArea.Item
|
||||
value={SchedulingType.COLLECTIVE}
|
||||
defaultChecked={type === SchedulingType.COLLECTIVE}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("collective")}</strong>
|
||||
<p>{t("collective_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
value={SchedulingType.ROUND_ROBIN}
|
||||
defaultChecked={type === SchedulingType.ROUND_ROBIN}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
<TextField
|
||||
label={t("title")}
|
||||
placeholder={t("quick_chat")}
|
||||
{...register("title")}
|
||||
onChange={(e) => {
|
||||
form.setValue("title", e?.target.value);
|
||||
if (form.formState.touchedFields["slug"] === undefined) {
|
||||
form.setValue("slug", slugify(e?.target.value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
|
||||
<TextField
|
||||
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
|
||||
required
|
||||
addOnLeading={<>/{pageSlug}/</>}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
|
||||
</>
|
||||
}
|
||||
{...register("slug")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextAreaField
|
||||
label={t("description")}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
{...register("description")}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<TextField
|
||||
type="number"
|
||||
required
|
||||
min="10"
|
||||
placeholder="15"
|
||||
label={t("length")}
|
||||
className="pr-20"
|
||||
{...register("length", { valueAsNumber: true })}
|
||||
addOnSuffix={t("minutes")}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{teamId && (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="schedulingType" className="block text-sm font-bold text-gray-700">
|
||||
{t("scheduling_type")}
|
||||
</label>
|
||||
{form.formState.errors.schedulingType && (
|
||||
<Alert
|
||||
className="mt-1"
|
||||
severity="error"
|
||||
message={form.formState.errors.schedulingType.message}
|
||||
/>
|
||||
)}
|
||||
<RadioArea.Group
|
||||
{...register("schedulingType")}
|
||||
onChange={(val) => form.setValue("schedulingType", val as SchedulingType)}
|
||||
className="relative mt-1 flex space-x-6 rounded-sm rtl:space-x-reverse">
|
||||
<RadioArea.Item
|
||||
value={SchedulingType.COLLECTIVE}
|
||||
defaultChecked={type === SchedulingType.COLLECTIVE}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("collective")}</strong>
|
||||
<p>{t("collective_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
value={SchedulingType.ROUND_ROBIN}
|
||||
defaultChecked={type === SchedulingType.ROUND_ROBIN}
|
||||
className="w-1/2 text-sm">
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
</RadioArea.Group>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<DialogClose />
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<Button type="submit" loading={createMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<DialogClose />
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,137 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
Form,
|
||||
showToast,
|
||||
TextAreaField,
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
|
||||
const DuplicateDialog = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
// react hook form
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
id: Number(router.query.id as string) || -1,
|
||||
title: (router.query.title as string) || "",
|
||||
slug: t("event_type_duplicate_copy_text", { slug: router.query.slug as string }),
|
||||
description: (router.query.description as string) || "",
|
||||
length: Number(router.query.length) || 30,
|
||||
},
|
||||
});
|
||||
const { register } = form;
|
||||
|
||||
const duplicateMutation = trpc.viewer.eventTypes.duplicate.useMutation({
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.replace("/event-types/" + eventType.id);
|
||||
showToast(t("event_type_created_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "BAD_REQUEST") {
|
||||
const message = `${err.data.code}: URL already exists.`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
const message = `${err.data.code}: You are not able to create this event`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const pageSlug = router.query.eventPage;
|
||||
return (
|
||||
<Dialog
|
||||
name="duplicate-event-type"
|
||||
clearQueryParamsOnClose={["description", "title", "length", "slug", "name", "id"]}>
|
||||
<DialogContent type="creation" className="overflow-y-auto" title="Duplicate Event Type">
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
duplicateMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-6">
|
||||
<TextField
|
||||
label={t("title")}
|
||||
placeholder={t("quick_chat")}
|
||||
{...register("title")}
|
||||
onChange={(e) => {
|
||||
form.setValue("title", e?.target.value);
|
||||
if (form.formState.touchedFields["slug"] === undefined) {
|
||||
form.setValue("slug", slugify(e?.target.value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL !== undefined &&
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL?.length >= 21 ? (
|
||||
<TextField
|
||||
label={`${t("url")}: ${process.env.NEXT_PUBLIC_WEBSITE_URL}`}
|
||||
required
|
||||
addOnLeading={<>/{pageSlug}/</>}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<TextField
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<>
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/{pageSlug}/
|
||||
</>
|
||||
}
|
||||
{...register("slug")}
|
||||
/>
|
||||
)}
|
||||
|
||||
<TextAreaField
|
||||
label={t("description")}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
{...register("description")}
|
||||
/>
|
||||
|
||||
<div className="relative">
|
||||
<TextField
|
||||
type="number"
|
||||
required
|
||||
min="10"
|
||||
placeholder="15"
|
||||
label={t("length")}
|
||||
className="pr-20"
|
||||
{...register("length", { valueAsNumber: true })}
|
||||
addOnSuffix={t("minutes")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-8 flex flex-row-reverse gap-x-2">
|
||||
<Button type="submit" loading={duplicateMutation.isLoading}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
<DialogClose />
|
||||
</div>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export { DuplicateDialog };
|
|
@ -99,6 +99,14 @@ const EventTypeUpdateInput = _EventTypeModel
|
|||
})
|
||||
);
|
||||
|
||||
const EventTypeDuplicateInput = z.object({
|
||||
id: z.number(),
|
||||
slug: z.string(),
|
||||
title: z.string(),
|
||||
description: z.string(),
|
||||
length: z.number(),
|
||||
});
|
||||
|
||||
const eventOwnerProcedure = authedProcedure.use(async ({ ctx, rawInput, next }) => {
|
||||
// Prevent non-owners to update/delete a team event
|
||||
const event = await ctx.prisma.eventType.findUnique({
|
||||
|
@ -598,4 +606,112 @@ export const eventTypesRouter = router({
|
|||
id,
|
||||
};
|
||||
}),
|
||||
duplicate: eventOwnerProcedure.input(EventTypeDuplicateInput.strict()).mutation(async ({ ctx, input }) => {
|
||||
const { id: originalEventTypeId, title: newEventTitle, slug: newSlug } = input;
|
||||
const eventType = await ctx.prisma.eventType.findUnique({
|
||||
where: {
|
||||
id: originalEventTypeId,
|
||||
},
|
||||
include: {
|
||||
customInputs: true,
|
||||
schedule: true,
|
||||
users: true,
|
||||
team: true,
|
||||
workflows: true,
|
||||
webhooks: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!eventType) {
|
||||
throw new TRPCError({ code: "NOT_FOUND" });
|
||||
}
|
||||
|
||||
// Validate user is owner of event type or in the team
|
||||
if (eventType.userId !== ctx.user.id) {
|
||||
if (eventType.teamId) {
|
||||
const isMember = await ctx.prisma.membership.findFirst({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
teamId: eventType.teamId,
|
||||
},
|
||||
});
|
||||
if (!isMember) {
|
||||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
}
|
||||
throw new TRPCError({ code: "FORBIDDEN" });
|
||||
}
|
||||
|
||||
const {
|
||||
customInputs,
|
||||
users,
|
||||
locations,
|
||||
team,
|
||||
recurringEvent,
|
||||
bookingLimits,
|
||||
metadata,
|
||||
workflows,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
id: _id,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
webhooks: _webhooks,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
schedule: _schedule,
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore - not typed correctly as its set on SSR
|
||||
descriptionAsSafeHTML: _descriptionAsSafeHTML,
|
||||
...rest
|
||||
} = eventType;
|
||||
|
||||
const data: Prisma.EventTypeCreateInput = {
|
||||
...rest,
|
||||
title: newEventTitle,
|
||||
slug: newSlug,
|
||||
locations: locations ?? undefined,
|
||||
team: team ? { connect: { id: team.id } } : undefined,
|
||||
users: users ? { connect: users.map((user) => ({ id: user.id })) } : undefined,
|
||||
recurringEvent: recurringEvent || undefined,
|
||||
bookingLimits: bookingLimits ?? undefined,
|
||||
metadata: metadata === null ? Prisma.DbNull : metadata,
|
||||
};
|
||||
|
||||
const newEventType = await ctx.prisma.eventType.create({ data });
|
||||
|
||||
// Create custom inputs
|
||||
if (customInputs) {
|
||||
const customInputsData = customInputs.map((customInput) => {
|
||||
const { id: _, options, ...rest } = customInput;
|
||||
return {
|
||||
options: options ?? undefined,
|
||||
...rest,
|
||||
eventTypeId: newEventType.id,
|
||||
};
|
||||
});
|
||||
await ctx.prisma.eventTypeCustomInput.createMany({
|
||||
data: customInputsData,
|
||||
});
|
||||
}
|
||||
|
||||
if (workflows.length > 0) {
|
||||
const workflowIds = workflows.map((workflow) => {
|
||||
return { id: workflow.workflowId };
|
||||
});
|
||||
|
||||
const eventUpdateData: Prisma.EventTypeUpdateInput = {
|
||||
workflows: {
|
||||
connect: workflowIds,
|
||||
},
|
||||
};
|
||||
await ctx.prisma.eventType.update({
|
||||
where: {
|
||||
id: newEventType.id,
|
||||
},
|
||||
data: eventUpdateData,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
eventType: newEventType,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import * as DialogPrimitive from "@radix-ui/react-dialog";
|
||||
import { useRouter } from "next/router";
|
||||
import React, { forwardRef, HTMLProps, ReactNode, useState } from "react";
|
||||
import React, { ReactNode, useState } from "react";
|
||||
import { Icon } from "react-feather";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
|
Loading…
Reference in New Issue