feat: Add step in team creation form for selecting event type and create a team event (#15286)
* feat: Create team event on a new team creation * fix * fixed failed tests * move team event type form * fix type error * requested chnages * update * some update and refactor * fix e2e test * Update useCreateEventType.ts * update * fix type error * fix e2e * update --------- Co-authored-by: Somay Chauhan <somaychauhan98@gmail.com> Co-authored-by: Hariom Balhara <hariombalhara@gmail.com> Co-authored-by: Keith Williams <keithwillcode@gmail.com> Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
This commit is contained in:
co-authored by
Somay Chauhan
Hariom Balhara
Keith Williams
Udit Takkar
parent
6b57dd6794
commit
fced9a6355
@@ -0,0 +1,11 @@
|
||||
import LegacyPage, { GetLayout } from "@pages/settings/teams/[id]/event-type";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("add_new_team_event_type"),
|
||||
(t) => t("new_event_type_to_book_description")
|
||||
);
|
||||
|
||||
export default WithLayout({ Page: LegacyPage, getLayout: GetLayout })<"P">;
|
||||
@@ -0,0 +1,86 @@
|
||||
"use client";
|
||||
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { WizardLayout } from "@calcom/ui";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export const CreateTeamEventType = () => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const teamId = searchParams?.get("id") ? Number(searchParams.get("id")) : -1;
|
||||
|
||||
const onSuccessMutation = () => {
|
||||
router.push(`/settings/teams/${teamId}/profile`);
|
||||
};
|
||||
|
||||
const onErrorMutation = (err: string) => {
|
||||
showToast(err, "error");
|
||||
};
|
||||
|
||||
const SubmitButton = (isPending: boolean) => {
|
||||
return (
|
||||
<Button
|
||||
data-testid="finish-button"
|
||||
type="submit"
|
||||
color="primary"
|
||||
className="w-full justify-center"
|
||||
disabled={isPending}>
|
||||
{t("finish")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TeamEventTypeForm
|
||||
isTeamAdminOrOwner={true}
|
||||
teamId={teamId}
|
||||
SubmitButton={SubmitButton}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
isInfiniteScrollEnabled={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const TeamEventTypePage = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("add_new_team_event_type")}</title>
|
||||
<meta name="description" content={t("new_event_type_to_book_description")} />
|
||||
</Head>
|
||||
<CreateTeamEventType />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const GetLayout = (page: React.ReactElement) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const teamId = searchParams?.get("id") ? Number(searchParams.get("id")) : -1;
|
||||
|
||||
return (
|
||||
<WizardLayout
|
||||
currentStep={3}
|
||||
maxSteps={3}
|
||||
isOptionalCallback={() => {
|
||||
router.push(`/settings/teams/${teamId}/profile`);
|
||||
}}>
|
||||
{page}
|
||||
</WizardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
TeamEventTypePage.getLayout = GetLayout;
|
||||
TeamEventTypePage.PageWrapper = PageWrapper;
|
||||
|
||||
export default TeamEventTypePage;
|
||||
@@ -22,7 +22,7 @@ const OnboardTeamMembersPage = () => {
|
||||
};
|
||||
|
||||
export const GetLayout = (page: React.ReactElement) => (
|
||||
<WizardLayout currentStep={2} maxSteps={2}>
|
||||
<WizardLayout currentStep={2} maxSteps={3}>
|
||||
{page}
|
||||
</WizardLayout>
|
||||
);
|
||||
|
||||
@@ -63,7 +63,7 @@ const CreateNewTeamPage = () => {
|
||||
};
|
||||
export const LayoutWrapper = (page: React.ReactElement) => {
|
||||
return (
|
||||
<WizardLayout currentStep={1} maxSteps={2}>
|
||||
<WizardLayout currentStep={1} maxSteps={3}>
|
||||
{page}
|
||||
</WizardLayout>
|
||||
);
|
||||
|
||||
@@ -38,10 +38,15 @@ test.describe("Managed Event Types", () => {
|
||||
await page.getByTestId("invite-new-member-button").click();
|
||||
// wait for the second member to be added to the pending-member-list.
|
||||
await page.getByTestId("pending-member-list").locator("li:nth-child(2)").waitFor();
|
||||
// and publish
|
||||
await page.locator("[data-testid=publish-button]").click();
|
||||
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
// Going to create an event type
|
||||
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/event-type$/i);
|
||||
// create a round-robin event type and finish
|
||||
await expect(page.locator('button[value="ROUND_ROBIN"]')).toBeVisible();
|
||||
await page.click('button[value="ROUND_ROBIN"]');
|
||||
await page.fill("[name=title]", "roundRobin");
|
||||
await page.getByTestId("finish-button").click();
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
// Going to create an managed event type
|
||||
await page.goto("/event-types");
|
||||
await page.getByTestId("new-event-type").click();
|
||||
await page.getByTestId("option-team-1").click();
|
||||
|
||||
@@ -64,13 +64,20 @@ test.describe("Teams", () => {
|
||||
await prisma.user.delete({ where: { email: inviteeEmail } });
|
||||
});
|
||||
|
||||
await test.step("Can finish team creation", async () => {
|
||||
await test.step("Can finish team invitation", async () => {
|
||||
await page.getByTestId("publish-button").click();
|
||||
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/event-type$/i);
|
||||
});
|
||||
|
||||
await test.step("Can finish team creation", async () => {
|
||||
await expect(page.locator('button[value="ROUND_ROBIN"]')).toBeVisible();
|
||||
await page.click('button[value="ROUND_ROBIN"]');
|
||||
await page.fill("[name=title]", "roundRobin");
|
||||
await page.getByTestId("finish-button").click();
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
});
|
||||
|
||||
await test.step("Can disband team", async () => {
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
await page.getByTestId("disband-team-button").click();
|
||||
await page.getByTestId("dialog-confirmation").click();
|
||||
await page.waitForURL("/teams");
|
||||
|
||||
@@ -68,7 +68,9 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
|
||||
await test.step("Finishing brings you to team profile page", async () => {
|
||||
await page.locator("[data-testid=publish-button]").click();
|
||||
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/event-type$/i);
|
||||
await page.locator("[data-testid=handle-later-button]").click();
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
});
|
||||
|
||||
await test.step("Can disband team", async () => {
|
||||
@@ -220,7 +222,9 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members.*$/i);
|
||||
// Click text=Continue
|
||||
await page.locator("[data-testid=publish-button]").click();
|
||||
await expect(page).toHaveURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/event-type*$/i);
|
||||
await page.locator("[data-testid=handle-later-button]").click();
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
|
||||
});
|
||||
|
||||
await test.step("Can access user and team with same slug", async () => {
|
||||
|
||||
@@ -199,13 +199,13 @@ export const AddNewTeamMembersForm = ({
|
||||
className="w-full justify-center"
|
||||
disabled={publishTeamMutation.isPending}
|
||||
onClick={() => {
|
||||
let uri = `/settings/teams/${teamId}/profile`;
|
||||
let uri = `/settings/teams/${teamId}/event-type`;
|
||||
if (isOrg) {
|
||||
uri = `/settings/organizations/${teamId}/add-teams`;
|
||||
}
|
||||
router.push(uri);
|
||||
}}>
|
||||
{isOrg ? t("continue") : t("finish")}
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { EventType } from "@prisma/client";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Form, TextField, Tooltip } from "@calcom/ui";
|
||||
import { Alert, RadioGroup as RadioArea } from "@calcom/ui";
|
||||
|
||||
type props = {
|
||||
isTeamAdminOrOwner: boolean;
|
||||
teamId: number;
|
||||
SubmitButton: (isPending: boolean) => ReactNode;
|
||||
onSuccessMutation: (eventType: EventType) => void;
|
||||
onErrorMutation: (message: string) => void;
|
||||
isInfiniteScrollEnabled: boolean;
|
||||
};
|
||||
export const TeamEventTypeForm = ({
|
||||
isTeamAdminOrOwner,
|
||||
teamId,
|
||||
SubmitButton,
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
isInfiniteScrollEnabled,
|
||||
}: props) => {
|
||||
const { t } = useLocale();
|
||||
const orgBranding = useOrgBranding();
|
||||
const { data: team } = trpc.viewer.teams.getMinimal.useQuery(
|
||||
{ teamId, isOrg: false },
|
||||
{ enabled: !!teamId }
|
||||
);
|
||||
const urlPrefix = orgBranding?.fullDomain ?? process.env.NEXT_PUBLIC_WEBSITE_URL;
|
||||
|
||||
const { form, createMutation, isManagedEventType } = useCreateEventType(
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
isInfiniteScrollEnabled
|
||||
);
|
||||
const { register, setValue, formState } = form;
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-6 pb-11">
|
||||
<TextField
|
||||
type="hidden"
|
||||
labelProps={{ style: { display: "none" } }}
|
||||
{...register("teamId", { valueAsNumber: true })}
|
||||
value={teamId}
|
||||
/>
|
||||
<TextField
|
||||
label={t("title")}
|
||||
placeholder={t("quick_chat")}
|
||||
data-testid="event-type-quick-chat"
|
||||
{...register("title")}
|
||||
onChange={(e) => {
|
||||
form.setValue("title", e?.target.value);
|
||||
if (formState.touchedFields["slug"] === undefined) {
|
||||
form.setValue("slug", slugify(e?.target.value));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{urlPrefix && urlPrefix.length >= 21 ? (
|
||||
<div>
|
||||
<TextField
|
||||
label={`${t("url")}: ${urlPrefix}`}
|
||||
required
|
||||
addOnLeading={
|
||||
<Tooltip content={!isManagedEventType ? `team/${team?.slug}` : t("username_placeholder")}>
|
||||
<span className="max-w-24 md:max-w-56">
|
||||
/{!isManagedEventType ? `team/${team?.slug}` : t("username_placeholder")}/
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
|
||||
{isManagedEventType && (
|
||||
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<TextField
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<Tooltip
|
||||
content={`${urlPrefix}/${
|
||||
!isManagedEventType ? `team/${team?.slug}` : t("username_placeholder")
|
||||
}/`}>
|
||||
<span className="max-w-24 md:max-w-56">
|
||||
{urlPrefix}/{!isManagedEventType ? `team/${team?.slug}` : t("username_placeholder")}/
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
{isManagedEventType && (
|
||||
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4">
|
||||
<label htmlFor="schedulingType" className="text-default block text-sm font-bold">
|
||||
{t("assignment")}
|
||||
</label>
|
||||
{formState.errors.schedulingType && (
|
||||
<Alert className="mt-1" severity="error" message={formState.errors.schedulingType.message} />
|
||||
)}
|
||||
<RadioArea.Group
|
||||
onValueChange={(val: SchedulingType) => {
|
||||
setValue("schedulingType", val);
|
||||
}}
|
||||
className={classNames("mt-1 flex gap-4", isTeamAdminOrOwner && "flex-col")}>
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.COLLECTIVE}
|
||||
className={classNames("w-full text-sm", !isTeamAdminOrOwner && "w-1/2")}
|
||||
classNames={{ container: classNames(isTeamAdminOrOwner && "w-full") }}>
|
||||
<strong className="mb-1 block">{t("collective")}</strong>
|
||||
<p>{t("collective_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.ROUND_ROBIN}
|
||||
className={classNames("text-sm", !isTeamAdminOrOwner && "w-1/2")}
|
||||
classNames={{ container: classNames(isTeamAdminOrOwner && "w-full") }}>
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
{isTeamAdminOrOwner && (
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.MANAGED}
|
||||
className={classNames("text-sm", !isTeamAdminOrOwner && "w-1/2")}
|
||||
classNames={{ container: classNames(isTeamAdminOrOwner && "w-full") }}
|
||||
data-testid="managed-event-type">
|
||||
<strong className="mb-1 block">{t("managed_event")}</strong>
|
||||
<p>{t("managed_event_description")}</p>
|
||||
</RadioArea.Item>
|
||||
)}
|
||||
</RadioArea.Group>
|
||||
</div>
|
||||
</div>
|
||||
{SubmitButton(createMutation.isPending)}
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
@@ -1,24 +1,19 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { EventType } from "@prisma/client";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { TeamEventTypeForm } from "@calcom/features/ee/teams/components/TeamEventTypeForm";
|
||||
import { useCreateEventType } from "@calcom/lib/hooks/useCreateEventType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
import { unlockedManagedEventTypeProps } from "@calcom/prisma/zod-utils";
|
||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
@@ -26,7 +21,6 @@ import {
|
||||
DialogFooter,
|
||||
Editor,
|
||||
Form,
|
||||
RadioGroup as RadioArea,
|
||||
showToast,
|
||||
TextField,
|
||||
Tooltip,
|
||||
@@ -80,7 +74,6 @@ export default function CreateEventTypeDialog({
|
||||
}[];
|
||||
isInfiniteScrollEnabled: boolean;
|
||||
}) {
|
||||
const utils = trpc.useUtils();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [firstRender, setFirstRender] = useState(true);
|
||||
@@ -91,70 +84,44 @@ export default function CreateEventTypeDialog({
|
||||
} = useTypedQuery(querySchema);
|
||||
|
||||
const teamProfile = profileOptions.find((profile) => profile.teamId === teamId);
|
||||
const form = useForm<z.infer<typeof createEventTypeInput>>({
|
||||
defaultValues: {
|
||||
length: 15,
|
||||
},
|
||||
resolver: zodResolver(createEventTypeInput),
|
||||
});
|
||||
|
||||
const schedulingTypeWatch = form.watch("schedulingType");
|
||||
const isManagedEventType = schedulingTypeWatch === SchedulingType.MANAGED;
|
||||
|
||||
useEffect(() => {
|
||||
if (isManagedEventType) {
|
||||
form.setValue("metadata.managedEventConfig.unlockedFields", unlockedManagedEventTypeProps);
|
||||
} else {
|
||||
form.setValue("metadata", null);
|
||||
}
|
||||
}, [schedulingTypeWatch]);
|
||||
|
||||
const { register } = form;
|
||||
|
||||
const isAdmin =
|
||||
const isTeamAdminOrOwner =
|
||||
teamId !== undefined &&
|
||||
(teamProfile?.membershipRole === MembershipRole.OWNER ||
|
||||
teamProfile?.membershipRole === MembershipRole.ADMIN);
|
||||
|
||||
const createMutation = trpc.viewer.eventTypes.create.useMutation({
|
||||
onSuccess: async ({ eventType }) => {
|
||||
await router.replace(`/event-types/${eventType.id}${teamId ? "?tabName=team" : ""}`);
|
||||
const onSuccessMutation = (eventType: EventType) => {
|
||||
router.replace(`/event-types/${eventType.id}${teamId ? "?tabName=team" : ""}`);
|
||||
showToast(
|
||||
t("event_type_created_successfully", {
|
||||
eventTypeTitle: eventType.title,
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
};
|
||||
|
||||
if (isInfiniteScrollEnabled) {
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.fetchInfinite({
|
||||
group: { teamId: eventType.teamId, parentId: eventType.parentId },
|
||||
limit: 10,
|
||||
});
|
||||
} else {
|
||||
await utils.viewer.eventTypes.getByViewer.invalidate();
|
||||
}
|
||||
const onErrorMutation = (err: string) => {
|
||||
showToast(err, "error");
|
||||
};
|
||||
|
||||
showToast(
|
||||
t("event_type_created_successfully", {
|
||||
eventTypeTitle: eventType.title,
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
form.reset();
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
const SubmitButton = (isPending: boolean) => {
|
||||
return (
|
||||
<DialogFooter showDivider>
|
||||
<DialogClose />
|
||||
<Button type="submit" loading={isPending}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
);
|
||||
};
|
||||
|
||||
if (err.data?.code === "BAD_REQUEST") {
|
||||
const message = `${err.data.code}: ${t("error_event_type_url_duplicate")}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
const message = `${err.data.code}: ${t("error_event_type_unauthorized_create")}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
const { form, createMutation, isManagedEventType } = useCreateEventType(
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
isInfiniteScrollEnabled
|
||||
);
|
||||
|
||||
const { register } = form;
|
||||
const urlPrefix = orgBranding?.fullDomain ?? process.env.NEXT_PUBLIC_WEBSITE_URL;
|
||||
|
||||
return (
|
||||
@@ -175,76 +142,82 @@ export default function CreateEventTypeDialog({
|
||||
enableOverflow
|
||||
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 pb-11">
|
||||
{teamId && (
|
||||
{teamId ? (
|
||||
<TeamEventTypeForm
|
||||
isTeamAdminOrOwner={isTeamAdminOrOwner}
|
||||
teamId={teamId}
|
||||
SubmitButton={SubmitButton}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
isInfiniteScrollEnabled={isInfiniteScrollEnabled}
|
||||
/>
|
||||
) : (
|
||||
<Form
|
||||
form={form}
|
||||
handleSubmit={(values) => {
|
||||
createMutation.mutate(values);
|
||||
}}>
|
||||
<div className="mt-3 space-y-6 pb-11">
|
||||
<TextField
|
||||
type="hidden"
|
||||
labelProps={{ style: { display: "none" } }}
|
||||
{...register("teamId", { valueAsNumber: true })}
|
||||
value={teamId}
|
||||
label={t("title")}
|
||||
placeholder={t("quick_chat")}
|
||||
data-testid="event-type-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));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<TextField
|
||||
label={t("title")}
|
||||
placeholder={t("quick_chat")}
|
||||
data-testid="event-type-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));
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{urlPrefix && urlPrefix.length >= 21 ? (
|
||||
<div>
|
||||
<TextField
|
||||
label={`${t("url")}: ${urlPrefix}`}
|
||||
required
|
||||
addOnLeading={
|
||||
<Tooltip content={!isManagedEventType ? pageSlug : t("username_placeholder")}>
|
||||
<span className="max-w-24 md:max-w-56">
|
||||
/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
{urlPrefix && urlPrefix.length >= 21 ? (
|
||||
<div>
|
||||
<TextField
|
||||
label={`${t("url")}: ${urlPrefix}`}
|
||||
required
|
||||
addOnLeading={
|
||||
<Tooltip content={!isManagedEventType ? pageSlug : t("username_placeholder")}>
|
||||
<span className="max-w-24 md:max-w-56">
|
||||
/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
|
||||
{isManagedEventType && (
|
||||
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<TextField
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<Tooltip
|
||||
content={`${urlPrefix}/${!isManagedEventType ? pageSlug : t("username_placeholder")}/`}>
|
||||
<span className="max-w-24 md:max-w-56">
|
||||
{urlPrefix}/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
{...register("slug")}
|
||||
/>
|
||||
{isManagedEventType && (
|
||||
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!teamId && (
|
||||
{isManagedEventType && (
|
||||
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<TextField
|
||||
label={t("url")}
|
||||
required
|
||||
addOnLeading={
|
||||
<Tooltip
|
||||
content={`${urlPrefix}/${
|
||||
!isManagedEventType ? pageSlug : t("username_placeholder")
|
||||
}/`}>
|
||||
<span className="max-w-24 md:max-w-56">
|
||||
{urlPrefix}/{!isManagedEventType ? pageSlug : t("username_placeholder")}/
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
{...register("slug")}
|
||||
onChange={(e) => {
|
||||
form.setValue("slug", slugify(e?.target.value), { shouldTouch: true });
|
||||
}}
|
||||
/>
|
||||
{isManagedEventType && (
|
||||
<p className="mt-2 text-sm text-gray-600">{t("managed_event_url_clarification")}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<>
|
||||
<Editor
|
||||
getText={() => md.render(form.getValues("description") || "")}
|
||||
@@ -269,65 +242,10 @@ export default function CreateEventTypeDialog({
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{teamId && (
|
||||
<div className="mb-4">
|
||||
<label htmlFor="schedulingType" className="text-default block text-sm font-bold">
|
||||
{t("assignment")}
|
||||
</label>
|
||||
{form.formState.errors.schedulingType && (
|
||||
<Alert
|
||||
className="mt-1"
|
||||
severity="error"
|
||||
message={form.formState.errors.schedulingType.message}
|
||||
/>
|
||||
)}
|
||||
<RadioArea.Group
|
||||
onValueChange={(val: SchedulingType) => {
|
||||
form.setValue("schedulingType", val);
|
||||
}}
|
||||
className={classNames("mt-1 flex gap-4", isAdmin && "flex-col")}>
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.COLLECTIVE}
|
||||
className={classNames("w-full text-sm", !isAdmin && "w-1/2")}
|
||||
classNames={{ container: classNames(isAdmin && "w-full") }}>
|
||||
<strong className="mb-1 block">{t("collective")}</strong>
|
||||
<p>{t("collective_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.ROUND_ROBIN}
|
||||
className={classNames("text-sm", !isAdmin && "w-1/2")}
|
||||
classNames={{ container: classNames(isAdmin && "w-full") }}>
|
||||
<strong className="mb-1 block">{t("round_robin")}</strong>
|
||||
<p>{t("round_robin_description")}</p>
|
||||
</RadioArea.Item>
|
||||
<>
|
||||
{isAdmin && (
|
||||
<RadioArea.Item
|
||||
{...register("schedulingType")}
|
||||
value={SchedulingType.MANAGED}
|
||||
className={classNames("text-sm", !isAdmin && "w-1/2")}
|
||||
classNames={{ container: classNames(isAdmin && "w-full") }}
|
||||
data-testid="managed-event-type">
|
||||
<strong className="mb-1 block">{t("managed_event")}</strong>
|
||||
<p>{t("managed_event_description")}</p>
|
||||
</RadioArea.Item>
|
||||
)}
|
||||
</>
|
||||
</RadioArea.Group>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogFooter showDivider>
|
||||
<DialogClose />
|
||||
<Button type="submit" loading={createMutation.isPending}>
|
||||
{t("continue")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</div>
|
||||
{SubmitButton(createMutation.isPending)}
|
||||
</Form>
|
||||
)}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { EventType } from "@prisma/client";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { unlockedManagedEventTypeProps } from "@calcom/prisma/zod-utils";
|
||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
export const useCreateEventType = (
|
||||
onSuccessMutation: (eventType: EventType) => void,
|
||||
onErrorMutation: (message: string) => void,
|
||||
isInfiniteScrollEnabled: boolean
|
||||
) => {
|
||||
const utils = trpc.useUtils();
|
||||
const { t } = useLocale();
|
||||
const form = useForm<z.infer<typeof createEventTypeInput>>({
|
||||
defaultValues: {
|
||||
length: 15,
|
||||
},
|
||||
resolver: zodResolver(createEventTypeInput),
|
||||
});
|
||||
|
||||
const schedulingTypeWatch = form.watch("schedulingType");
|
||||
const isManagedEventType = schedulingTypeWatch === SchedulingType.MANAGED;
|
||||
|
||||
useEffect(() => {
|
||||
if (isManagedEventType) {
|
||||
form.setValue("metadata.managedEventConfig.unlockedFields", unlockedManagedEventTypeProps);
|
||||
} else {
|
||||
form.setValue("metadata", null);
|
||||
}
|
||||
}, [schedulingTypeWatch]);
|
||||
|
||||
const createMutation = trpc.viewer.eventTypes.create.useMutation({
|
||||
onSuccess: async ({ eventType }) => {
|
||||
onSuccessMutation(eventType);
|
||||
|
||||
if (isInfiniteScrollEnabled) {
|
||||
await utils.viewer.eventTypes.getEventTypesFromGroup.fetchInfinite({
|
||||
group: { teamId: eventType.teamId, parentId: eventType.parentId },
|
||||
limit: 10,
|
||||
});
|
||||
} else {
|
||||
await utils.viewer.eventTypes.getByViewer.invalidate();
|
||||
}
|
||||
|
||||
form.reset();
|
||||
},
|
||||
onError: (err) => {
|
||||
let error = err.message;
|
||||
if (err instanceof HttpError) {
|
||||
error = `${err.statusCode}: ${err.message}`;
|
||||
}
|
||||
|
||||
if (err.data?.code === "BAD_REQUEST") {
|
||||
error = `${err.data.code}: ${t("error_event_type_url_duplicate")}`;
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
error = `${err.data.code}: ${t("error_event_type_unauthorized_create")}`;
|
||||
}
|
||||
onErrorMutation(error);
|
||||
},
|
||||
});
|
||||
|
||||
return { form, createMutation, isManagedEventType };
|
||||
};
|
||||
@@ -63,7 +63,7 @@ export function WizardLayout({
|
||||
</div>
|
||||
{isOptionalCallback && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<Button color="minimal" onClick={isOptionalCallback}>
|
||||
<Button data-testid="handle-later-button" color="minimal" onClick={isOptionalCallback}>
|
||||
{t("ill_do_this_later")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user