Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99fe67b879 | ||
|
|
2685e8154f | ||
|
|
4308ea8d52 | ||
|
|
358aa8be2b | ||
|
|
b20c0df4e8 | ||
|
|
09af14b3f4 | ||
|
|
77df4117d0 | ||
|
|
84f7924215 | ||
|
|
c8554e6878 | ||
|
|
5993e0842c | ||
|
|
7179b09d7b | ||
|
|
325dc9805f | ||
|
|
0f6a678b74 |
No files matched your search
+6
-3
@@ -22,10 +22,13 @@ CALCOM_LICENSE_KEY=
|
|||||||
|
|
||||||
# - DATABASE ************************************************************************************************
|
# - DATABASE ************************************************************************************************
|
||||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||||
# Opt-in to a dedicated connection pool for Prisma using Prisma Data Proxy
|
|
||||||
# Cold boots will be faster and you will be able to scale your DB independently of your application.
|
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||||
|
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||||
PRISMA_GENERATE_DATAPROXY=false
|
# PRISMA_GENERATE_DATAPROXY=true
|
||||||
|
PRISMA_GENERATE_DATAPROXY=
|
||||||
|
|
||||||
# ***********************************************************************************************************
|
# ***********************************************************************************************************
|
||||||
|
|
||||||
# - SHARED **************************************************************************************************
|
# - SHARED **************************************************************************************************
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import type { IframeHTMLAttributes } from "react";
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
@@ -45,12 +46,12 @@ const Component = ({
|
|||||||
tos,
|
tos,
|
||||||
privacy,
|
privacy,
|
||||||
isProOnly,
|
isProOnly,
|
||||||
images,
|
descriptionItems,
|
||||||
isTemplate,
|
isTemplate,
|
||||||
dependencies,
|
dependencies,
|
||||||
}: Parameters<typeof App>[0]) => {
|
}: Parameters<typeof App>[0]) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const hasImages = images && images.length > 0;
|
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const mutation = useAddAppMutation(null, {
|
const mutation = useAddAppMutation(null, {
|
||||||
@@ -94,17 +95,25 @@ const Component = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||||
{hasImages && (
|
{hasDescriptionItems && (
|
||||||
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||||
{images ? (
|
{descriptionItems ? (
|
||||||
images.map((img) => (
|
descriptionItems.map((descriptionItem, index) =>
|
||||||
<img
|
typeof descriptionItem === "object" ? (
|
||||||
key={img}
|
<div
|
||||||
src={img}
|
key={`iframe-${index}`}
|
||||||
alt={`Screenshot of app ${name}`}
|
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
|
||||||
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
|
<iframe allowFullScreen {...descriptionItem.iframe} />
|
||||||
/>
|
</div>
|
||||||
))
|
) : (
|
||||||
|
<img
|
||||||
|
key={descriptionItem}
|
||||||
|
src={descriptionItem}
|
||||||
|
alt={`Screenshot of app ${name}`}
|
||||||
|
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
)
|
||||||
) : (
|
) : (
|
||||||
<SkeletonText />
|
<SkeletonText />
|
||||||
)}
|
)}
|
||||||
@@ -113,7 +122,7 @@ const Component = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
|
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
|
||||||
hasImages && "lg:ml-8"
|
hasDescriptionItems && "lg:ml-8"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-8 flex pt-4">
|
<div className="mb-8 flex pt-4">
|
||||||
<header>
|
<header>
|
||||||
@@ -354,7 +363,7 @@ export default function App(props: {
|
|||||||
privacy?: string;
|
privacy?: string;
|
||||||
licenseRequired: AppType["licenseRequired"];
|
licenseRequired: AppType["licenseRequired"];
|
||||||
isProOnly: AppType["isProOnly"];
|
isProOnly: AppType["isProOnly"];
|
||||||
images?: string[];
|
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
|
||||||
isTemplate?: boolean;
|
isTemplate?: boolean;
|
||||||
disableInstall?: boolean;
|
disableInstall?: boolean;
|
||||||
dependencies?: string[];
|
dependencies?: string[];
|
||||||
|
|||||||
@@ -135,7 +135,6 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
<li key={idx}>
|
<li key={idx}>
|
||||||
<Badge
|
<Badge
|
||||||
variant="gray"
|
variant="gray"
|
||||||
size="lg"
|
|
||||||
className={classNames(
|
className={classNames(
|
||||||
duration === dur.toString()
|
duration === dur.toString()
|
||||||
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ const BookingFields = ({
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { watch, setValue } = useFormContext();
|
const { watch, setValue } = useFormContext();
|
||||||
const locationResponse = watch("responses.location");
|
const locationResponse = watch("responses.location");
|
||||||
|
const currentView = rescheduleUid ? "reschedule" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
|
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
|
||||||
@@ -87,12 +88,16 @@ const BookingFields = ({
|
|||||||
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
|
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
|
||||||
let readOnly =
|
let readOnly =
|
||||||
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
|
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
|
||||||
|
|
||||||
let noLabel = false;
|
let noLabel = false;
|
||||||
let hidden = !!field.hidden;
|
let hidden = !!field.hidden;
|
||||||
|
const fieldViews = field.views;
|
||||||
|
|
||||||
|
if (fieldViews && !fieldViews.find((view) => view.id === currentView)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
if (field.name === SystemField.Enum.rescheduleReason) {
|
if (field.name === SystemField.Enum.rescheduleReason) {
|
||||||
if (!rescheduleUid) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
|
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
|
||||||
readOnly = false;
|
readOnly = false;
|
||||||
}
|
}
|
||||||
@@ -292,7 +297,10 @@ const BookingPage = ({
|
|||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
const date = asStringOrNull(router.query.date);
|
const date = asStringOrNull(router.query.date);
|
||||||
const querySchema = getBookingResponsesPartialSchema({
|
const querySchema = getBookingResponsesPartialSchema({
|
||||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
eventType: {
|
||||||
|
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||||
|
},
|
||||||
|
view: rescheduleUid ? "reschedule" : "booking",
|
||||||
});
|
});
|
||||||
|
|
||||||
const parsedQuery = querySchema.parse({
|
const parsedQuery = querySchema.parse({
|
||||||
@@ -372,7 +380,8 @@ const BookingPage = ({
|
|||||||
const bookingFormSchema = z
|
const bookingFormSchema = z
|
||||||
.object({
|
.object({
|
||||||
responses: getBookingResponsesSchema({
|
responses: getBookingResponsesSchema({
|
||||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
eventType: { bookingFields: getBookingFieldsWithSystemFields(eventType) },
|
||||||
|
view: rescheduleUid ? "reschedule" : "booking",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|||||||
@@ -112,7 +112,11 @@ export const getBookingWithResponses = <
|
|||||||
return {
|
return {
|
||||||
...booking,
|
...booking,
|
||||||
responses: getBookingResponsesPartialSchema({
|
responses: getBookingResponsesPartialSchema({
|
||||||
bookingFields: eventType.bookingFields,
|
eventType: {
|
||||||
|
bookingFields: eventType.bookingFields,
|
||||||
|
},
|
||||||
|
// An existing booking can have data from any number of views, so the schema should consider ALL_VIEWS
|
||||||
|
view: "ALL_VIEWS",
|
||||||
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
|
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.7.0",
|
"version": "2.7.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import matter from "gray-matter";
|
|||||||
import MarkdownIt from "markdown-it";
|
import MarkdownIt from "markdown-it";
|
||||||
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -13,6 +14,23 @@ import App from "@components/apps/App";
|
|||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||||
|
|
||||||
|
const sourceSchema = z.object({
|
||||||
|
content: z.string(),
|
||||||
|
data: z.object({
|
||||||
|
description: z.string().optional(),
|
||||||
|
items: z
|
||||||
|
.array(
|
||||||
|
z.union([
|
||||||
|
z.string(),
|
||||||
|
z.object({
|
||||||
|
iframe: z.object({ src: z.string() }),
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||||
return (
|
return (
|
||||||
<App
|
<App
|
||||||
@@ -33,7 +51,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
|||||||
email={data.email}
|
email={data.email}
|
||||||
licenseRequired={data.licenseRequired}
|
licenseRequired={data.licenseRequired}
|
||||||
isProOnly={data.isProOnly}
|
isProOnly={data.isProOnly}
|
||||||
images={source.data?.items as string[] | undefined}
|
descriptionItems={source.data?.items as string[] | undefined}
|
||||||
isTemplate={data.isTemplate}
|
isTemplate={data.isTemplate}
|
||||||
dependencies={data.dependencies}
|
dependencies={data.dependencies}
|
||||||
// tos="https://zoom.us/terms"
|
// tos="https://zoom.us/terms"
|
||||||
@@ -85,10 +103,11 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||||||
source = singleApp.description;
|
source = singleApp.description;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { content, data } = matter(source);
|
const result = matter(source);
|
||||||
|
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||||
if (data.items) {
|
if (data.items) {
|
||||||
data.items = data.items.map((item: string) => {
|
data.items = data.items.map((item) => {
|
||||||
if (!item.includes("/api/app-store")) {
|
if (typeof item === "string" && !item.includes("/api/app-store")) {
|
||||||
// Make relative paths absolute
|
// Make relative paths absolute
|
||||||
return `/api/app-store/${appDirname}/${item}`;
|
return `/api/app-store/${appDirname}/${item}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export default function Logout(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Button href="/auth/login" passHref className="flex w-full justify-center">
|
<Button href="/auth/login" className="flex w-full justify-center">
|
||||||
{t("go_back_login")}
|
{t("go_back_login")}
|
||||||
</Button>
|
</Button>
|
||||||
</AuthContainer>
|
</AuthContainer>
|
||||||
|
|||||||
@@ -272,10 +272,8 @@ export default function Success(props: SuccessProps) {
|
|||||||
const isCancelled =
|
const isCancelled =
|
||||||
status === "CANCELLED" ||
|
status === "CANCELLED" ||
|
||||||
status === "REJECTED" ||
|
status === "REJECTED" ||
|
||||||
(isCancellationMode &&
|
(!!seatReferenceUid &&
|
||||||
(!!seatReferenceUid
|
!bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid));
|
||||||
? !bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid)
|
|
||||||
: !userIsOwner));
|
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -606,7 +604,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
|
|
||||||
{!props.recurringBookings && (
|
{!props.recurringBookings && (
|
||||||
<span className="text-bookinglight inline text-gray-700">
|
<span className="text-bookinglight inline text-gray-700">
|
||||||
<span className="underline">
|
<span className="underline" data-testid="reschedule-link">
|
||||||
<Link
|
<Link
|
||||||
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
|
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
|
||||||
legacyBehavior>
|
legacyBehavior>
|
||||||
|
|||||||
@@ -351,11 +351,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
||||||
{type.hidden && (
|
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
|
||||||
<Badge variant="gray" size="lg">
|
|
||||||
{t("hidden")}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
<Tooltip content={t("show_eventtype_on_profile")}>
|
<Tooltip content={t("show_eventtype_on_profile")}>
|
||||||
<div className="self-center rounded-md p-2 hover:bg-gray-200">
|
<div className="self-center rounded-md p-2 hover:bg-gray-200">
|
||||||
<Switch
|
<Switch
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ const PasswordView = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { data: user } = trpc.viewer.me.useQuery();
|
const { data: user } = trpc.viewer.me.useQuery();
|
||||||
const metadata = userMetadata.parse(user?.metadata);
|
const metadata = userMetadata.safeParse(user?.metadata);
|
||||||
|
const sessionTimeout = metadata.success ? metadata.data?.sessionTimeout : undefined;
|
||||||
|
|
||||||
const sessionMutation = trpc.viewer.updateProfile.useMutation({
|
const sessionMutation = trpc.viewer.updateProfile.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
@@ -36,10 +37,10 @@ const PasswordView = () => {
|
|||||||
const previousValue = utils.viewer.me.getData();
|
const previousValue = utils.viewer.me.getData();
|
||||||
const previousMetadata = userMetadata.parse(previousValue?.metadata);
|
const previousMetadata = userMetadata.parse(previousValue?.metadata);
|
||||||
|
|
||||||
if (previousValue && metadata?.sessionTimeout) {
|
if (previousValue && sessionTimeout) {
|
||||||
utils.viewer.me.setData(undefined, {
|
utils.viewer.me.setData(undefined, {
|
||||||
...previousValue,
|
...previousValue,
|
||||||
metadata: { ...previousMetadata, sessionTimeout: metadata?.sessionTimeout },
|
metadata: { ...previousMetadata, sessionTimeout: sessionTimeout },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return { previousValue };
|
return { previousValue };
|
||||||
@@ -81,19 +82,19 @@ const PasswordView = () => {
|
|||||||
defaultValues: {
|
defaultValues: {
|
||||||
oldPassword: "",
|
oldPassword: "",
|
||||||
newPassword: "",
|
newPassword: "",
|
||||||
sessionTimeout: metadata?.sessionTimeout,
|
sessionTimeout,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionTimeoutWatch = formMethods.watch("sessionTimeout");
|
const sessionTimeoutWatch = formMethods.watch("sessionTimeout");
|
||||||
|
|
||||||
const handleSubmit = (values: ChangePasswordSessionFormValues) => {
|
const handleSubmit = (values: ChangePasswordSessionFormValues) => {
|
||||||
const { oldPassword, newPassword, sessionTimeout } = values;
|
const { oldPassword, newPassword, sessionTimeout: newSessionTimeout } = values;
|
||||||
if (oldPassword && newPassword) {
|
if (oldPassword && newPassword) {
|
||||||
passwordMutation.mutate({ oldPassword, newPassword });
|
passwordMutation.mutate({ oldPassword, newPassword });
|
||||||
}
|
}
|
||||||
if (metadata?.sessionTimeout !== sessionTimeout) {
|
if (sessionTimeout !== newSessionTimeout) {
|
||||||
sessionMutation.mutate({ metadata: { ...metadata, sessionTimeout } });
|
sessionMutation.mutate({ metadata: { ...metadata, sessionTimeout: newSessionTimeout } });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -106,6 +107,7 @@ const PasswordView = () => {
|
|||||||
|
|
||||||
const passwordMinLength = data?.user.role === "USER" ? 7 : 15;
|
const passwordMinLength = data?.user.role === "USER" ? 7 : 15;
|
||||||
const isUser = data?.user.role === "USER";
|
const isUser = data?.user.role === "USER";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title={t("password")} description={t("password_description")} />
|
<Meta title={t("password")} description={t("password_description")} />
|
||||||
@@ -176,8 +178,8 @@ const PasswordView = () => {
|
|||||||
<Select
|
<Select
|
||||||
options={timeoutOptions}
|
options={timeoutOptions}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
metadata?.sessionTimeout
|
sessionTimeout
|
||||||
? timeoutOptions.find((tmo) => tmo.value === metadata.sessionTimeout)
|
? timeoutOptions.find((tmo) => tmo.value === sessionTimeout)
|
||||||
: timeoutOptions[1]
|
: timeoutOptions[1]
|
||||||
}
|
}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
|||||||
@@ -33,56 +33,108 @@ test.describe("Manage Booking Questions", () => {
|
|||||||
await firstEventTypeElement.click();
|
await firstEventTypeElement.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
||||||
await addQuestionAndSave({
|
});
|
||||||
page,
|
});
|
||||||
question: {
|
|
||||||
name: "how_are_you",
|
|
||||||
type: "Name",
|
|
||||||
label: "How are you?",
|
|
||||||
placeholder: "I'm fine, thanks",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
test.describe("For Team EventType", () => {
|
||||||
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
test("Do a booking with a user added question and verify a few thing in b/w", async ({
|
||||||
const userFieldLocator = allFieldsLocator.nth(5);
|
page,
|
||||||
|
users,
|
||||||
|
context,
|
||||||
|
}, testInfo) => {
|
||||||
|
// Considering there are many steps in it, it would need more than default test timeout
|
||||||
|
test.setTimeout(testInfo.timeout * 3);
|
||||||
|
const user = await createAndLoginUserWithEventTypes({ users });
|
||||||
|
|
||||||
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
const webhookReceiver = await addWebhook(user);
|
||||||
// There are 2 labels right now. Will be one in future. The second one is hidden
|
|
||||||
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
await test.step("Go to First Team Event", async () => {
|
||||||
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
|
const $eventTypes = page.locator("[data-testid=event-types]").nth(1).locator("li a");
|
||||||
});
|
const firstEventTypeElement = $eventTypes.first();
|
||||||
|
|
||||||
|
await firstEventTypeElement.click();
|
||||||
});
|
});
|
||||||
|
|
||||||
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
||||||
await toggleQuestionAndSave({
|
});
|
||||||
name: "how_are_you",
|
});
|
||||||
page,
|
});
|
||||||
});
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
|
||||||
await expect(formBuilderFieldLocator).toBeHidden();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Show Question Again", async () => {
|
async function runTestStepsCommonForTeamAndUserEventType(
|
||||||
await toggleQuestionAndSave({
|
page: Page,
|
||||||
name: "how_are_you",
|
context: PlaywrightTestArgs["context"],
|
||||||
page,
|
webhookReceiver: {
|
||||||
});
|
port: number;
|
||||||
});
|
close: () => import("http").Server;
|
||||||
|
requestList: (import("http").IncomingMessage & { body?: unknown })[];
|
||||||
|
url: string;
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
await page.click('[href$="tabName=advanced"]');
|
||||||
|
|
||||||
await test.step('Try to book without providing "How are you?" response', async () => {
|
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
await addQuestionAndSave({
|
||||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
page,
|
||||||
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
question: {
|
||||||
});
|
name: "how_are_you",
|
||||||
});
|
type: "Name",
|
||||||
|
label: "How are you?",
|
||||||
|
placeholder: "I'm fine, thanks",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
await test.step("Do a booking", async () => {
|
await doOnFreshPreview(page, context, async (page) => {
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
||||||
|
const userFieldLocator = allFieldsLocator.nth(5);
|
||||||
|
|
||||||
|
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
||||||
|
// There are 2 labels right now. Will be one in future. The second one is hidden
|
||||||
|
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
||||||
|
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
||||||
|
await toggleQuestionAndSave({
|
||||||
|
name: "how_are_you",
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
await doOnFreshPreview(page, context, async (page) => {
|
||||||
|
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||||
|
await expect(formBuilderFieldLocator).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Show Question Again", async () => {
|
||||||
|
await toggleQuestionAndSave({
|
||||||
|
name: "how_are_you",
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step('Try to book without providing "How are you?" response', async () => {
|
||||||
|
await doOnFreshPreview(page, context, async (page) => {
|
||||||
|
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
||||||
|
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await test.step("Make rescheduleReason required - It won't be required for a fresh booking", async () => {
|
||||||
|
await toggleQuestionRequireStatusAndSave({
|
||||||
|
required: true,
|
||||||
|
name: "rescheduleReason",
|
||||||
|
page,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const previewTabPage =
|
||||||
|
await test.step("Do a booking and notice that we can book without giving a value for rescheduleReason", async () => {
|
||||||
|
return await doOnFreshPreview(
|
||||||
|
page,
|
||||||
|
context,
|
||||||
|
async (page) => {
|
||||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||||
await expect(formBuilderFieldLocator).toBeVisible();
|
await expect(formBuilderFieldLocator).toBeVisible();
|
||||||
expect(
|
expect(
|
||||||
@@ -133,146 +185,16 @@ test.describe("Manage Booking Questions", () => {
|
|||||||
value: "I am great!",
|
value: "I am great!",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
true
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test.describe("For Team EventType", () => {
|
|
||||||
test("Do a booking with a user added question and verify a few thing in b/w", async ({
|
|
||||||
page,
|
|
||||||
users,
|
|
||||||
context,
|
|
||||||
}, testInfo) => {
|
|
||||||
// Considering there are many steps in it, it would need more than default test timeout
|
|
||||||
test.setTimeout(testInfo.timeout * 3);
|
|
||||||
const user = await createAndLoginUserWithEventTypes({ users });
|
|
||||||
|
|
||||||
const webhookReceiver = await addWebhook(user);
|
|
||||||
|
|
||||||
await test.step("Go to First Team Event", async () => {
|
|
||||||
const $eventTypes = page.locator("[data-testid=event-types]").nth(1).locator("li a");
|
|
||||||
const firstEventTypeElement = $eventTypes.first();
|
|
||||||
|
|
||||||
await firstEventTypeElement.click();
|
|
||||||
});
|
|
||||||
|
|
||||||
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
async function runTestStepsCommonForTeamAndUserEventType(
|
|
||||||
page: Page,
|
|
||||||
context: PlaywrightTestArgs["context"],
|
|
||||||
webhookReceiver: {
|
|
||||||
port: number;
|
|
||||||
close: () => import("http").Server;
|
|
||||||
requestList: (import("http").IncomingMessage & { body?: unknown })[];
|
|
||||||
url: string;
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
|
||||||
await addQuestionAndSave({
|
|
||||||
page,
|
|
||||||
question: {
|
|
||||||
name: "how_are_you",
|
|
||||||
type: "Name",
|
|
||||||
label: "How are you?",
|
|
||||||
placeholder: "I'm fine, thanks",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
|
||||||
const userFieldLocator = allFieldsLocator.nth(5);
|
|
||||||
|
|
||||||
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
|
||||||
// There are 2 labels right now. Will be one in future. The second one is hidden
|
|
||||||
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
|
||||||
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
|
||||||
await toggleQuestionAndSave({
|
|
||||||
name: "how_are_you",
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
|
||||||
await expect(formBuilderFieldLocator).toBeHidden();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Show Question Again", async () => {
|
|
||||||
await toggleQuestionAndSave({
|
|
||||||
name: "how_are_you",
|
|
||||||
page,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step('Try to book without providing "How are you?" response', async () => {
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
|
||||||
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await test.step("Do a booking", async () => {
|
|
||||||
await doOnFreshPreview(page, context, async (page) => {
|
|
||||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
|
||||||
await expect(formBuilderFieldLocator).toBeVisible();
|
|
||||||
expect(await formBuilderFieldLocator.locator('[name="how_are_you"]').getAttribute("placeholder")).toBe(
|
|
||||||
"I'm fine, thanks"
|
|
||||||
);
|
);
|
||||||
expect(await formBuilderFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
|
||||||
await formBuilderFieldLocator.locator('[name="how_are_you"]').fill("I am great!");
|
|
||||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
|
||||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await page.locator('[data-testid="field-response"][data-fob-field="how_are_you"]').innerText()
|
|
||||||
).toBe("I am great!");
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(webhookReceiver.requestList.length).toBe(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
const [request] = webhookReceiver.requestList;
|
|
||||||
|
|
||||||
const payload = (request.body as any).payload as any;
|
|
||||||
|
|
||||||
expect(payload.responses).toMatchObject({
|
|
||||||
email: {
|
|
||||||
label: "email_address",
|
|
||||||
value: "booker@example.com",
|
|
||||||
},
|
|
||||||
how_are_you: {
|
|
||||||
label: "How are you?",
|
|
||||||
value: "I am great!",
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
label: "your_name",
|
|
||||||
value: "Booker",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(payload.location).toBe("integrations:daily");
|
|
||||||
|
|
||||||
expect(payload.attendees[0]).toMatchObject({
|
|
||||||
name: "Booker",
|
|
||||||
email: "booker@example.com",
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(payload.userFieldsResponses).toMatchObject({
|
|
||||||
how_are_you: {
|
|
||||||
label: "How are you?",
|
|
||||||
value: "I am great!",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await test.step("Do a reschedule and notice that we can't book without giving a value for rescheduleReason", async () => {
|
||||||
|
const page = previewTabPage;
|
||||||
|
await rescheduleFromTheLinkOnPage({ page });
|
||||||
|
await page.pause();
|
||||||
|
await expectErrorToBeThereFor({ page, name: "rescheduleReason" });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -333,7 +255,6 @@ async function addQuestionAndSave({
|
|||||||
required?: boolean;
|
required?: boolean;
|
||||||
};
|
};
|
||||||
}) {
|
}) {
|
||||||
await page.click('[href$="tabName=advanced"]');
|
|
||||||
await page.click('[data-testid="add-field"]');
|
await page.click('[data-testid="add-field"]');
|
||||||
|
|
||||||
if (question.type !== undefined) {
|
if (question.type !== undefined) {
|
||||||
@@ -379,11 +300,15 @@ async function expectErrorToBeThereFor({ page, name }: { page: Page; name: strin
|
|||||||
async function doOnFreshPreview(
|
async function doOnFreshPreview(
|
||||||
page: Page,
|
page: Page,
|
||||||
context: PlaywrightTestArgs["context"],
|
context: PlaywrightTestArgs["context"],
|
||||||
callback: (page: Page) => Promise<void>
|
callback: (page: Page) => Promise<void>,
|
||||||
|
persistTab = false
|
||||||
) {
|
) {
|
||||||
const previewTabPage = await openBookingFormInPreviewTab(context, page);
|
const previewTabPage = await openBookingFormInPreviewTab(context, page);
|
||||||
await callback(previewTabPage);
|
await callback(previewTabPage);
|
||||||
await previewTabPage.close();
|
if (!persistTab) {
|
||||||
|
await previewTabPage.close();
|
||||||
|
}
|
||||||
|
return previewTabPage;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleQuestionAndSave({ name, page }: { name: string; page: Page }) {
|
async function toggleQuestionAndSave({ name, page }: { name: string; page: Page }) {
|
||||||
@@ -391,6 +316,25 @@ async function toggleQuestionAndSave({ name, page }: { name: string; page: Page
|
|||||||
await saveEventType(page);
|
await saveEventType(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function toggleQuestionRequireStatusAndSave({
|
||||||
|
required,
|
||||||
|
name,
|
||||||
|
page,
|
||||||
|
}: {
|
||||||
|
required: boolean;
|
||||||
|
name: string;
|
||||||
|
page: Page;
|
||||||
|
}) {
|
||||||
|
await page.locator(`[data-testid="field-${name}"]`).locator('[data-testid="edit-field-action"]').click();
|
||||||
|
await page
|
||||||
|
.locator('[data-testid="edit-field-dialog"]')
|
||||||
|
.locator('[data-testid="field-required"] button')
|
||||||
|
.locator(`text=${required ? "Yes" : "No"}`)
|
||||||
|
.click();
|
||||||
|
await page.locator('[data-testid="field-add-save"]').click();
|
||||||
|
await saveEventType(page);
|
||||||
|
}
|
||||||
|
|
||||||
async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<typeof createUsersFixture> }) {
|
async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<typeof createUsersFixture> }) {
|
||||||
const user = await users.create(null, {
|
const user = await users.create(null, {
|
||||||
hasTeam: true,
|
hasTeam: true,
|
||||||
@@ -399,6 +343,16 @@ async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<t
|
|||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function rescheduleFromTheLinkOnPage({ page }: { page: Page }) {
|
||||||
|
await page.locator('[data-testid="reschedule-link"]').click();
|
||||||
|
await page.waitForLoadState();
|
||||||
|
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||||
|
await page.waitForNavigation({
|
||||||
|
url: (url) => url.pathname.endsWith("/book"),
|
||||||
|
});
|
||||||
|
await page.click('[data-testid="confirm-reschedule-button"]');
|
||||||
|
}
|
||||||
|
|
||||||
async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"], page: Page) {
|
async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"], page: Page) {
|
||||||
const previewTabPromise = context.waitForEvent("page");
|
const previewTabPromise = context.waitForEvent("page");
|
||||||
await page.locator('[data-testid="preview-button"]').click();
|
await page.locator('[data-testid="preview-button"]').click();
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
reminders: {
|
reminders: {
|
||||||
useDefault: true,
|
useDefault: true,
|
||||||
},
|
},
|
||||||
guestsCanSeeOtherGuests: calEventRaw.seatsShowAttendees,
|
guestsCanSeeOtherGuests: !!calEventRaw.seatsPerTimeSlot ? calEventRaw.seatsShowAttendees : true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (calEventRaw.location) {
|
if (calEventRaw.location) {
|
||||||
@@ -193,7 +193,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
reminders: {
|
reminders: {
|
||||||
useDefault: true,
|
useDefault: true,
|
||||||
},
|
},
|
||||||
guestsCanSeeOtherGuests: event.seatsShowAttendees,
|
guestsCanSeeOtherGuests: !!event.seatsPerTimeSlot ? event.seatsShowAttendees : true,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (event.location) {
|
if (event.location) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { Calendar as OfficeCalendar, User } from "@microsoft/microsoft-graph-types-beta";
|
import type { Calendar as OfficeCalendar, User } from "@microsoft/microsoft-graph-types-beta";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||||
@@ -38,6 +39,14 @@ interface IBatchResponse {
|
|||||||
responses: ISettledResponse[];
|
responses: ISettledResponse[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const refreshTokenResponseSchema = z.object({
|
||||||
|
access_token: z.string(),
|
||||||
|
expires_in: z
|
||||||
|
.number()
|
||||||
|
.transform((currentTimeOffsetInSeconds) => Math.round(+new Date() / 1000 + currentTimeOffsetInSeconds)),
|
||||||
|
refresh_token: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export default class Office365CalendarService implements Calendar {
|
export default class Office365CalendarService implements Calendar {
|
||||||
private url = "";
|
private url = "";
|
||||||
private integrationName = "";
|
private integrationName = "";
|
||||||
@@ -217,14 +226,7 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
client_secret,
|
client_secret,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const responseBody = await handleErrorsJson<{
|
const o365AuthCredentials = refreshTokenResponseSchema.parse(await handleErrorsJson(response));
|
||||||
access_token: string;
|
|
||||||
expires_in: number;
|
|
||||||
refresh_token: string;
|
|
||||||
}>(response);
|
|
||||||
o365AuthCredentials.access_token = responseBody.access_token;
|
|
||||||
o365AuthCredentials.refresh_token = responseBody.refresh_token;
|
|
||||||
o365AuthCredentials.expiry_date = Math.round(+new Date() / 1000 + responseBody.expires_in);
|
|
||||||
await prisma.credential.update({
|
await prisma.credential.update({
|
||||||
where: {
|
where: {
|
||||||
id: credential.id,
|
id: credential.id,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
---
|
---
|
||||||
description: Adds a link to copy Typeform Redirect URL to integrate with Routing Forms.
|
description: Adds a link to copy Typeform Redirect URL to integrate with Routing Forms.
|
||||||
items:
|
items:
|
||||||
|
- iframe: { src: https://www.youtube.com/embed/vcV-N2gLPZc }
|
||||||
- /api/app-store/typeform/copy-typeform-redirect-url.png
|
- /api/app-store/typeform/copy-typeform-redirect-url.png
|
||||||
- /api/app-store/typeform/how-it-looks-in-typeform.png
|
- /api/app-store/typeform/how-it-looks-in-typeform.png
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -60,7 +60,11 @@ export const BaseScheduledEmail = (
|
|||||||
subtitle={props.subtitle || <>{t("emailed_you_and_any_other_attendees")}</>}>
|
subtitle={props.subtitle || <>{t("emailed_you_and_any_other_attendees")}</>}>
|
||||||
{props.calEvent.cancellationReason && (
|
{props.calEvent.cancellationReason && (
|
||||||
<Info
|
<Info
|
||||||
label={t("cancellation_reason")}
|
label={t(
|
||||||
|
props.calEvent.cancellationReason.startsWith("$RCH$")
|
||||||
|
? "reschedule_reason"
|
||||||
|
: "cancellation_reason"
|
||||||
|
)}
|
||||||
description={
|
description={
|
||||||
!!props.calEvent.cancellationReason && props.calEvent.cancellationReason.replace("$RCH$", "")
|
!!props.calEvent.cancellationReason && props.calEvent.cancellationReason.replace("$RCH$", "")
|
||||||
} // Removing flag to distinguish reschedule from cancellation
|
} // Removing flag to distinguish reschedule from cancellation
|
||||||
|
|||||||
@@ -444,7 +444,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||||||
identityProvider: idP,
|
identityProvider: idP,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
identityProviderId: account.providerAccountId as string,
|
identityProviderId: account.providerAccountId,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
@@ -523,11 +523,11 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||||||
return "/auth/error?error=unverified-email";
|
return "/auth/error?error=unverified-email";
|
||||||
}
|
}
|
||||||
|
|
||||||
const existingUser = await prisma.user.findFirst({
|
let existingUser = await prisma.user.findFirst({
|
||||||
include: {
|
include: {
|
||||||
accounts: {
|
accounts: {
|
||||||
where: {
|
where: {
|
||||||
provider: idP,
|
provider: account.provider,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -537,6 +537,33 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* --- START FIX LEGACY ISSUE WHERE 'identityProviderId' was accidentally set to userId --- */
|
||||||
|
if (!existingUser) {
|
||||||
|
existingUser = await prisma.user.findFirst({
|
||||||
|
include: {
|
||||||
|
accounts: {
|
||||||
|
where: {
|
||||||
|
provider: account.provider,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
where: {
|
||||||
|
identityProvider: idP,
|
||||||
|
identityProviderId: String(user.id),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (existingUser) {
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: existingUser?.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
identityProviderId: account.providerAccountId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* --- END FIXES LEGACY ISSUE WHERE 'identityProviderId' was accidentally set to userId --- */
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
// In this case there's an existing user and their email address
|
// In this case there's an existing user and their email address
|
||||||
// hasn't changed since they last logged in.
|
// hasn't changed since they last logged in.
|
||||||
@@ -617,7 +644,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||||||
emailVerified: new Date(Date.now()),
|
emailVerified: new Date(Date.now()),
|
||||||
name: user.name,
|
name: user.name,
|
||||||
identityProvider: idP,
|
identityProvider: idP,
|
||||||
identityProviderId: String(user.id),
|
identityProviderId: account.providerAccountId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -640,7 +667,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||||||
password: null,
|
password: null,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
identityProvider: idP,
|
identityProvider: idP,
|
||||||
identityProviderId: String(user.id),
|
identityProviderId: account.providerAccountId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (existingUserWithEmail.twoFactorEnabled) {
|
if (existingUserWithEmail.twoFactorEnabled) {
|
||||||
@@ -664,7 +691,7 @@ export const AUTH_OPTIONS: AuthOptions = {
|
|||||||
name: user.name,
|
name: user.name,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
identityProvider: idP,
|
identityProvider: idP,
|
||||||
identityProviderId: String(user.id),
|
identityProviderId: account.providerAccountId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const SystemFieldsEditability: Record<z.infer<typeof SystemField>, Fields
|
|||||||
location: "system",
|
location: "system",
|
||||||
notes: "system-but-optional",
|
notes: "system-but-optional",
|
||||||
guests: "system-but-optional",
|
guests: "system-but-optional",
|
||||||
rescheduleReason: "system",
|
rescheduleReason: "system-but-optional",
|
||||||
smsReminderNumber: "system",
|
smsReminderNumber: "system",
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -252,6 +252,12 @@ export const ensureBookingInputsHaveSystemFields = ({
|
|||||||
name: "rescheduleReason",
|
name: "rescheduleReason",
|
||||||
defaultPlaceholder: "reschedule_placeholder",
|
defaultPlaceholder: "reschedule_placeholder",
|
||||||
required: false,
|
required: false,
|
||||||
|
views: [
|
||||||
|
{
|
||||||
|
id: "reschedule",
|
||||||
|
label: "Reschedule View",
|
||||||
|
},
|
||||||
|
],
|
||||||
sources: [
|
sources: [
|
||||||
{
|
{
|
||||||
label: "Default",
|
label: "Default",
|
||||||
@@ -264,9 +270,16 @@ export const ensureBookingInputsHaveSystemFields = ({
|
|||||||
|
|
||||||
const missingSystemBeforeFields = [];
|
const missingSystemBeforeFields = [];
|
||||||
for (const field of systemBeforeFields) {
|
for (const field of systemBeforeFields) {
|
||||||
|
const existingBookingFieldIndex = bookingFields.findIndex((f) => f.name === field.name);
|
||||||
// Only do a push, we must not update existing system fields as user could have modified any property in it,
|
// Only do a push, we must not update existing system fields as user could have modified any property in it,
|
||||||
if (!bookingFields.find((f) => f.name === field.name)) {
|
if (existingBookingFieldIndex === -1) {
|
||||||
missingSystemBeforeFields.push(field);
|
missingSystemBeforeFields.push(field);
|
||||||
|
} else {
|
||||||
|
// Adding the fields from Code first and then fields from DB. Allows, the code to push new properties to the field
|
||||||
|
bookingFields[existingBookingFieldIndex] = {
|
||||||
|
...field,
|
||||||
|
...bookingFields[existingBookingFieldIndex],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -310,9 +323,16 @@ export const ensureBookingInputsHaveSystemFields = ({
|
|||||||
|
|
||||||
const missingSystemAfterFields = [];
|
const missingSystemAfterFields = [];
|
||||||
for (const field of systemAfterFields) {
|
for (const field of systemAfterFields) {
|
||||||
|
const existingBookingFieldIndex = bookingFields.findIndex((f) => f.name === field.name);
|
||||||
// Only do a push, we must not update existing system fields as user could have modified any property in it,
|
// Only do a push, we must not update existing system fields as user could have modified any property in it,
|
||||||
if (!bookingFields.find((f) => f.name === field.name)) {
|
if (existingBookingFieldIndex === -1) {
|
||||||
missingSystemAfterFields.push(field);
|
missingSystemAfterFields.push(field);
|
||||||
|
} else {
|
||||||
|
bookingFields[existingBookingFieldIndex] = {
|
||||||
|
// Adding the fields from Code first and then fields from DB. Allows, the code to push new properties to the field
|
||||||
|
...field,
|
||||||
|
...bookingFields[existingBookingFieldIndex],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,32 @@
|
|||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
|
import type { ALL_VIEWS } from "@calcom/features/form-builder/FormBuilderFieldsSchema";
|
||||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
import { bookingResponses } from "@calcom/prisma/zod-utils";
|
import { bookingResponses } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
type EventType = Parameters<typeof preprocess>[0]["eventType"];
|
type EventType = Parameters<typeof preprocess>[0]["eventType"];
|
||||||
export const getBookingResponsesPartialSchema = (eventType: EventType) => {
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
type View = ALL_VIEWS | (string & {});
|
||||||
|
|
||||||
|
export const getBookingResponsesPartialSchema = ({
|
||||||
|
eventType,
|
||||||
|
view,
|
||||||
|
}: {
|
||||||
|
eventType: EventType;
|
||||||
|
view: View;
|
||||||
|
}) => {
|
||||||
const schema = bookingResponses.unwrap().partial().and(z.record(z.any()));
|
const schema = bookingResponses.unwrap().partial().and(z.record(z.any()));
|
||||||
|
|
||||||
return preprocess({ schema, eventType, isPartialSchema: true });
|
return preprocess({ schema, eventType, isPartialSchema: true, view });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Should be used when we know that not all fields responses are present
|
// Should be used when we know that not all fields responses are present
|
||||||
// - Can happen when we are parsing the prefill query string
|
// - Can happen when we are parsing the prefill query string
|
||||||
// - Can happen when we are parsing a booking's responses (which was created before we added a new required field)
|
// - Can happen when we are parsing a booking's responses (which was created before we added a new required field)
|
||||||
export default function getBookingResponsesSchema(eventType: EventType) {
|
export default function getBookingResponsesSchema({ eventType, view }: { eventType: EventType; view: View }) {
|
||||||
const schema = bookingResponses.and(z.record(z.any()));
|
const schema = bookingResponses.and(z.record(z.any()));
|
||||||
return preprocess({ schema, eventType, isPartialSchema: false });
|
return preprocess({ schema, eventType, isPartialSchema: false, view });
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Move preprocess of `booking.responses` to FormBuilder schema as that is going to parse the fields supported by FormBuilder
|
// TODO: Move preprocess of `booking.responses` to FormBuilder schema as that is going to parse the fields supported by FormBuilder
|
||||||
@@ -25,12 +35,14 @@ function preprocess<T extends z.ZodType>({
|
|||||||
schema,
|
schema,
|
||||||
eventType,
|
eventType,
|
||||||
isPartialSchema,
|
isPartialSchema,
|
||||||
|
view: currentView,
|
||||||
}: {
|
}: {
|
||||||
schema: T;
|
schema: T;
|
||||||
isPartialSchema: boolean;
|
isPartialSchema: boolean;
|
||||||
eventType: {
|
eventType: {
|
||||||
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
|
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
|
||||||
};
|
};
|
||||||
|
view: View;
|
||||||
}): z.ZodType<z.infer<T>, z.infer<T>, z.infer<T>> {
|
}): z.ZodType<z.infer<T>, z.infer<T>, z.infer<T>> {
|
||||||
const preprocessed = z.preprocess(
|
const preprocessed = z.preprocess(
|
||||||
(responses) => {
|
(responses) => {
|
||||||
@@ -42,8 +54,15 @@ function preprocess<T extends z.ZodType>({
|
|||||||
// If there is no response for the field, then we don't need to do any processing
|
// If there is no response for the field, then we don't need to do any processing
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Turn a boolean in string to a real boolean
|
const views = field.views;
|
||||||
|
const isFieldApplicableToCurrentView =
|
||||||
|
currentView === "ALL_VIEWS" ? true : views ? views.find((view) => view.id === currentView) : true;
|
||||||
|
if (!isFieldApplicableToCurrentView) {
|
||||||
|
// If the field is not applicable in the current view, then we don't need to do any processing
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (field.type === "boolean") {
|
if (field.type === "boolean") {
|
||||||
|
// Turn a boolean in string to a real boolean
|
||||||
newResponses[field.name] = value === "true" || value === true;
|
newResponses[field.name] = value === "true" || value === true;
|
||||||
}
|
}
|
||||||
// Make sure that the value is an array
|
// Make sure that the value is an array
|
||||||
@@ -76,8 +95,16 @@ function preprocess<T extends z.ZodType>({
|
|||||||
: z.string().refine((val) => isValidPhoneNumber(val));
|
: z.string().refine((val) => isValidPhoneNumber(val));
|
||||||
// Tag the message with the input name so that the message can be shown at appropriate place
|
// Tag the message with the input name so that the message can be shown at appropriate place
|
||||||
const m = (message: string) => `{${bookingField.name}}${message}`;
|
const m = (message: string) => `{${bookingField.name}}${message}`;
|
||||||
|
const views = bookingField.views;
|
||||||
|
const isFieldApplicableToCurrentView =
|
||||||
|
currentView === "ALL_VIEWS" ? true : views ? views.find((view) => view.id === currentView) : true;
|
||||||
// If the field is hidden, then it can never be required
|
// If the field is hidden, then it can never be required
|
||||||
const isRequired = bookingField.hidden ? false : bookingField.required;
|
const isRequired = bookingField.hidden
|
||||||
|
? false
|
||||||
|
: isFieldApplicableToCurrentView
|
||||||
|
? bookingField.required
|
||||||
|
: false;
|
||||||
|
|
||||||
if ((isPartialSchema || !isRequired) && value === undefined) {
|
if ((isPartialSchema || !isRequired) && value === undefined) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -377,7 +377,10 @@ function getBookingData({
|
|||||||
? extendedBookingCreateBody.merge(
|
? extendedBookingCreateBody.merge(
|
||||||
z.object({
|
z.object({
|
||||||
responses: getBookingResponsesSchema({
|
responses: getBookingResponsesSchema({
|
||||||
bookingFields: eventType.bookingFields,
|
eventType: {
|
||||||
|
bookingFields: eventType.bookingFields,
|
||||||
|
},
|
||||||
|
view: req.body.rescheduleUid ? "reschedule" : "booking",
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -689,18 +692,15 @@ async function handler(
|
|||||||
|
|
||||||
const guests = (reqGuests || []).reduce((guestArray, guest) => {
|
const guests = (reqGuests || []).reduce((guestArray, guest) => {
|
||||||
// If it's a team event, remove the team member from guests
|
// If it's a team event, remove the team member from guests
|
||||||
if (isTeamEventType) {
|
if (isTeamEventType && users.some((user) => user.email === guest)) {
|
||||||
if (users.some((user) => user.email === guest)) {
|
return guestArray;
|
||||||
return guestArray;
|
|
||||||
} else {
|
|
||||||
guestArray.push({
|
|
||||||
email: guest,
|
|
||||||
name: "",
|
|
||||||
timeZone: reqBody.timeZone,
|
|
||||||
language: { translate: tGuests, locale: "en" },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
guestArray.push({
|
||||||
|
email: guest,
|
||||||
|
name: "",
|
||||||
|
timeZone: reqBody.timeZone,
|
||||||
|
language: { translate: tGuests, locale: "en" },
|
||||||
|
});
|
||||||
return guestArray;
|
return guestArray;
|
||||||
}, [] as typeof invitee);
|
}, [] as typeof invitee);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -13,10 +14,6 @@ const SAMLSSO = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
if (!HOSTED_CAL_FEATURES) {
|
|
||||||
router.push("/404");
|
|
||||||
}
|
|
||||||
|
|
||||||
const teamId = Number(router.query.id);
|
const teamId = Number(router.query.id);
|
||||||
|
|
||||||
const { data: team, isLoading } = trpc.viewer.teams.get.useQuery(
|
const { data: team, isLoading } = trpc.viewer.teams.get.useQuery(
|
||||||
@@ -28,6 +25,12 @@ const SAMLSSO = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!HOSTED_CAL_FEATURES) {
|
||||||
|
router.push("/404");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <SkeletonLoader />;
|
return <SkeletonLoader />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -11,9 +12,11 @@ const SAMLSSO = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
if (HOSTED_CAL_FEATURES) {
|
useEffect(() => {
|
||||||
router.push("/404");
|
if (HOSTED_CAL_FEATURES) {
|
||||||
}
|
router.push("/404");
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full bg-white sm:mx-0 xl:mt-0">
|
<div className="w-full bg-white sm:mx-0 xl:mt-0">
|
||||||
|
|||||||
@@ -65,21 +65,21 @@ export const EventTypeDescription = ({
|
|||||||
{eventType.metadata?.multipleDuration ? (
|
{eventType.metadata?.multipleDuration ? (
|
||||||
eventType.metadata.multipleDuration.map((dur, idx) => (
|
eventType.metadata.multipleDuration.map((dur, idx) => (
|
||||||
<li key={idx}>
|
<li key={idx}>
|
||||||
<Badge variant="gray" size="lg" startIcon={FiClock}>
|
<Badge variant="gray" startIcon={FiClock}>
|
||||||
{dur}m
|
{dur}m
|
||||||
</Badge>
|
</Badge>
|
||||||
</li>
|
</li>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<li>
|
<li>
|
||||||
<Badge variant="gray" size="lg" startIcon={FiClock}>
|
<Badge variant="gray" startIcon={FiClock}>
|
||||||
{eventType.length}m
|
{eventType.length}m
|
||||||
</Badge>
|
</Badge>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{eventType.schedulingType && (
|
{eventType.schedulingType && (
|
||||||
<li>
|
<li>
|
||||||
<Badge variant="gray" size="lg" startIcon={FiUsers}>
|
<Badge variant="gray" startIcon={FiUsers}>
|
||||||
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
|
{eventType.schedulingType === SchedulingType.ROUND_ROBIN && t("round_robin")}
|
||||||
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
|
{eventType.schedulingType === SchedulingType.COLLECTIVE && t("collective")}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -87,7 +87,7 @@ export const EventTypeDescription = ({
|
|||||||
)}
|
)}
|
||||||
{recurringEvent?.count && recurringEvent.count > 0 && (
|
{recurringEvent?.count && recurringEvent.count > 0 && (
|
||||||
<li className="hidden xl:block">
|
<li className="hidden xl:block">
|
||||||
<Badge variant="gray" size="lg" startIcon={FiRefreshCw}>
|
<Badge variant="gray" startIcon={FiRefreshCw}>
|
||||||
{t("repeats_up_to", {
|
{t("repeats_up_to", {
|
||||||
count: recurringEvent.count,
|
count: recurringEvent.count,
|
||||||
})}
|
})}
|
||||||
@@ -96,7 +96,7 @@ export const EventTypeDescription = ({
|
|||||||
)}
|
)}
|
||||||
{stripeAppData.price > 0 && (
|
{stripeAppData.price > 0 && (
|
||||||
<li>
|
<li>
|
||||||
<Badge variant="gray" size="lg" startIcon={FiCreditCard}>
|
<Badge variant="gray" startIcon={FiCreditCard}>
|
||||||
<IntlProvider locale="en">
|
<IntlProvider locale="en">
|
||||||
<FormattedNumber
|
<FormattedNumber
|
||||||
value={stripeAppData.price / 100.0}
|
value={stripeAppData.price / 100.0}
|
||||||
@@ -109,7 +109,7 @@ export const EventTypeDescription = ({
|
|||||||
)}
|
)}
|
||||||
{eventType.requiresConfirmation && (
|
{eventType.requiresConfirmation && (
|
||||||
<li className="hidden xl:block">
|
<li className="hidden xl:block">
|
||||||
<Badge variant="gray" size="lg" startIcon={FiClipboard}>
|
<Badge variant="gray" startIcon={FiClipboard}>
|
||||||
{eventType.metadata?.requiresConfirmationThreshold
|
{eventType.metadata?.requiresConfirmationThreshold
|
||||||
? t("may_require_confirmation")
|
? t("may_require_confirmation")
|
||||||
: t("requires_confirmation")}
|
: t("requires_confirmation")}
|
||||||
@@ -119,7 +119,7 @@ export const EventTypeDescription = ({
|
|||||||
{/* TODO: Maybe add a tool tip to this? */}
|
{/* TODO: Maybe add a tool tip to this? */}
|
||||||
{eventType.requiresConfirmation || (recurringEvent?.count && recurringEvent.count) ? (
|
{eventType.requiresConfirmation || (recurringEvent?.count && recurringEvent.count) ? (
|
||||||
<li className="block xl:hidden">
|
<li className="block xl:hidden">
|
||||||
<Badge variant="gray" size="lg" startIcon={FiPlus}>
|
<Badge variant="gray" startIcon={FiPlus}>
|
||||||
<p>{[eventType.requiresConfirmation, recurringEvent?.count].filter(Boolean).length}</p>
|
<p>{[eventType.requiresConfirmation, recurringEvent?.count].filter(Boolean).length}</p>
|
||||||
</Badge>
|
</Badge>
|
||||||
</li>
|
</li>
|
||||||
@@ -128,7 +128,7 @@ export const EventTypeDescription = ({
|
|||||||
)}
|
)}
|
||||||
{eventType?.seatsPerTimeSlot ? (
|
{eventType?.seatsPerTimeSlot ? (
|
||||||
<li>
|
<li>
|
||||||
<Badge variant="gray" size="lg" startIcon={FiUser}>
|
<Badge variant="gray" startIcon={FiUser}>
|
||||||
<p>{t("event_type_seats", { numberOfSeats: eventType.seatsPerTimeSlot })} </p>
|
<p>{t("event_type_seats", { numberOfSeats: eventType.seatsPerTimeSlot })} </p>
|
||||||
</Badge>
|
</Badge>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -284,21 +284,25 @@ export const FormBuilder = function FormBuilder({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
key={index}
|
key={field.name}
|
||||||
data-testid={`field-${field.name}`}
|
data-testid={`field-${field.name}`}
|
||||||
className="group relative flex items-center justify-between border-b p-4 last:border-b-0">
|
className="group relative flex items-center justify-between border-b p-4 last:border-b-0">
|
||||||
<button
|
{index >= 1 && (
|
||||||
type="button"
|
<button
|
||||||
className="invisible absolute -left-[12px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex"
|
type="button"
|
||||||
onClick={() => swap(index, index - 1)}>
|
className="invisible absolute -left-[12px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex"
|
||||||
<FiArrowUp className="h-5 w-5" />
|
onClick={() => swap(index, index - 1)}>
|
||||||
</button>
|
<FiArrowUp className="h-5 w-5" />
|
||||||
<button
|
</button>
|
||||||
type="button"
|
)}
|
||||||
className="invisible absolute -left-[12px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex"
|
{index < fields.length - 1 && (
|
||||||
onClick={() => swap(index, index + 1)}>
|
<button
|
||||||
<FiArrowDown className="h-5 w-5" />
|
type="button"
|
||||||
</button>
|
className="invisible absolute -left-[12px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border bg-white p-1 text-gray-400 transition-all hover:border-transparent hover:text-black hover:shadow disabled:hover:border-inherit disabled:hover:text-gray-400 disabled:hover:shadow-none group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex"
|
||||||
|
onClick={() => swap(index, index + 1)}>
|
||||||
|
<FiArrowDown className="h-5 w-5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex flex-col lg:flex-row lg:items-center">
|
<div className="flex flex-col lg:flex-row lg:items-center">
|
||||||
<div className="text-sm font-semibold text-gray-700 ltr:mr-1 rtl:ml-1">
|
<div className="text-sm font-semibold text-gray-700 ltr:mr-1 rtl:ml-1">
|
||||||
@@ -335,6 +339,7 @@ export const FormBuilder = function FormBuilder({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
|
data-testid="edit-field-action"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
editField(index, field);
|
editField(index, field);
|
||||||
@@ -378,7 +383,7 @@ export const FormBuilder = function FormBuilder({
|
|||||||
fieldIndex: -1,
|
fieldIndex: -1,
|
||||||
})
|
})
|
||||||
}>
|
}>
|
||||||
<DialogContent enableOverflow>
|
<DialogContent enableOverflow data-testid="edit-field-dialog">
|
||||||
<DialogHeader title={t("add_a_booking_question")} subtitle={t("form_builder_field_add_subtitle")} />
|
<DialogHeader title={t("add_a_booking_question")} subtitle={t("form_builder_field_add_subtitle")} />
|
||||||
<div>
|
<div>
|
||||||
<Form
|
<Form
|
||||||
@@ -472,6 +477,7 @@ export const FormBuilder = function FormBuilder({
|
|||||||
render={({ field: { value, onChange } }) => {
|
render={({ field: { value, onChange } }) => {
|
||||||
return (
|
return (
|
||||||
<BooleanToggleGroupField
|
<BooleanToggleGroupField
|
||||||
|
data-testid="field-required"
|
||||||
disabled={fieldForm.getValues("editable") === "system"}
|
disabled={fieldForm.getValues("editable") === "system"}
|
||||||
value={value}
|
value={value}
|
||||||
onValueChange={(val) => {
|
onValueChange={(val) => {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const EditableSchema = z.enum([
|
|||||||
"user", // Fully editable
|
"user", // Fully editable
|
||||||
"user-readonly", // All fields are readOnly.
|
"user-readonly", // All fields are readOnly.
|
||||||
]);
|
]);
|
||||||
|
export type ALL_VIEWS = "ALL_VIEWS";
|
||||||
const fieldSchema = z.object({
|
const fieldSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
// TODO: We should make at least one of `defaultPlaceholder` and `placeholder` required. Do the same for label.
|
// TODO: We should make at least one of `defaultPlaceholder` and `placeholder` required. Do the same for label.
|
||||||
@@ -35,7 +35,14 @@ const fieldSchema = z.object({
|
|||||||
*/
|
*/
|
||||||
defaultLabel: z.string().optional(),
|
defaultLabel: z.string().optional(),
|
||||||
defaultPlaceholder: z.string().optional(),
|
defaultPlaceholder: z.string().optional(),
|
||||||
|
views: z
|
||||||
|
.object({
|
||||||
|
label: z.string(),
|
||||||
|
id: z.string(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
})
|
||||||
|
.array()
|
||||||
|
.optional(),
|
||||||
type: fieldTypeEnum,
|
type: fieldTypeEnum,
|
||||||
options: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
|
options: z.array(z.object({ label: z.string(), value: z.string() })).optional(),
|
||||||
optionsInputs: z
|
optionsInputs: z
|
||||||
|
|||||||
+192
-206
@@ -7,14 +7,17 @@ import {
|
|||||||
KBarSearch,
|
KBarSearch,
|
||||||
useKBar,
|
useKBar,
|
||||||
useMatches,
|
useMatches,
|
||||||
|
useRegisterActions,
|
||||||
} from "kbar";
|
} from "kbar";
|
||||||
|
import type { Action } from "kbar";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { isMac } from "@calcom/lib/isMac";
|
import { isMac } from "@calcom/lib/isMac";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
import { FiSearch, FiArrowUp, FiArrowDown, FiCornerDownLeft, FiCommand } from "@calcom/ui/components/icon";
|
import { FiSearch, FiArrowUp, FiArrowDown, FiCornerDownLeft, FiCommand } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -24,15 +27,6 @@ type shortcutArrayType = {
|
|||||||
|
|
||||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||||
type EventTypeGroup = EventTypeGroups[number];
|
type EventTypeGroup = EventTypeGroups[number];
|
||||||
type EventType = EventTypeGroup["eventTypes"][number];
|
|
||||||
|
|
||||||
type KBarAction = {
|
|
||||||
perform: () => Promise<boolean>;
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
section: string;
|
|
||||||
keywords: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getApps = Object.values(appStoreMetadata).map(({ name, slug }) => ({
|
const getApps = Object.values(appStoreMetadata).map(({ name, slug }) => ({
|
||||||
id: slug,
|
id: slug,
|
||||||
@@ -41,216 +35,208 @@ const getApps = Object.values(appStoreMetadata).map(({ name, slug }) => ({
|
|||||||
keywords: `app ${name}`,
|
keywords: `app ${name}`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const KBarRoot = ({ children }: { children: React.ReactNode }) => {
|
const useEventTypesAction = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = trpc.viewer.eventTypes.getByViewer.useQuery();
|
const { data } = trpc.viewer.eventTypes.getByViewer.useQuery();
|
||||||
|
const eventTypeActions = data?.eventTypeGroups.reduce<Action[]>((acc: Action[], group: EventTypeGroup) => {
|
||||||
|
const item: Action[] = group.eventTypes.map((item) => ({
|
||||||
|
id: `event-type-${item.id}`,
|
||||||
|
name: item.title,
|
||||||
|
section: "event_types_page_title",
|
||||||
|
keywords: "event types",
|
||||||
|
perform: () => router.push(`/event-types/${item.id}`),
|
||||||
|
}));
|
||||||
|
acc.push(...item);
|
||||||
|
return acc;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const actions = eventTypeActions?.length ? eventTypeActions : [];
|
||||||
|
|
||||||
|
return useRegisterActions(actions);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const KBarRoot = ({ children }: { children: React.ReactNode }) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
// grab link to events
|
// grab link to events
|
||||||
// quick nested actions would be extremely useful
|
// quick nested actions would be extremely useful
|
||||||
const appStoreActions = useMemo(
|
|
||||||
() => getApps.map((item) => ({ ...item, perform: () => router.push(`/apps/${item.id}`) })),
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!data) return null;
|
const actions = useMemo(() => {
|
||||||
|
const appStoreActions = getApps.map((item) => ({
|
||||||
const eventTypes =
|
...item,
|
||||||
data?.eventTypeGroups.reduce((acc: EventType[], group: EventTypeGroup): EventType[] => {
|
perform: () => router.push(`/apps/${item.id}`),
|
||||||
acc.push(...group.eventTypes);
|
}));
|
||||||
return acc;
|
return [
|
||||||
}, [] as EventType[]) || [];
|
{
|
||||||
|
id: "workflows",
|
||||||
const eventTypeActions =
|
name: "workflows",
|
||||||
eventTypes.map(
|
section: "workflows",
|
||||||
(item: EventType): KBarAction => ({
|
shortcut: ["w", "f"],
|
||||||
id: `event-type-${item.id}`,
|
keywords: "workflows automation",
|
||||||
name: item.title,
|
perform: () => router.push("/workflows"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "event-types",
|
||||||
|
name: "event_types_page_title",
|
||||||
section: "event_types_page_title",
|
section: "event_types_page_title",
|
||||||
|
shortcut: ["e", "t"],
|
||||||
keywords: "event types",
|
keywords: "event types",
|
||||||
perform: () => router.push(`/event-types/${item.id}`),
|
perform: () => router.push("/event-types"),
|
||||||
})
|
},
|
||||||
) || [];
|
{
|
||||||
|
id: "app-store",
|
||||||
const actions = [
|
name: "app_store",
|
||||||
// {
|
section: "apps",
|
||||||
// id: "toggle-idle",
|
shortcut: ["a", "s"],
|
||||||
// name: "Test Function",
|
keywords: "app store",
|
||||||
// section: "Status",
|
perform: () => router.push("/apps"),
|
||||||
// shortcut: ["t", "f"],
|
},
|
||||||
// keywords: "set yourself away bookings",
|
{
|
||||||
// perform: () => alert("Hello World"),
|
id: "upcoming-bookings",
|
||||||
// },
|
name: "upcoming",
|
||||||
|
section: "bookings",
|
||||||
{
|
shortcut: ["u", "b"],
|
||||||
id: "workflows",
|
keywords: "upcoming bookings",
|
||||||
name: "workflows",
|
perform: () => router.push("/bookings/upcoming"),
|
||||||
section: "workflows",
|
},
|
||||||
shortcut: ["w", "f"],
|
{
|
||||||
keywords: "workflows automation",
|
id: "recurring-bookings",
|
||||||
perform: () => router.push("/workflows"),
|
name: "recurring",
|
||||||
},
|
section: "bookings",
|
||||||
{
|
shortcut: ["r", "b"],
|
||||||
id: "event-types",
|
keywords: "recurring bookings",
|
||||||
name: "event_types_page_title",
|
perform: () => router.push("/bookings/recurring"),
|
||||||
section: "event_types_page_title",
|
},
|
||||||
shortcut: ["e", "t"],
|
{
|
||||||
keywords: "event types",
|
id: "past-bookings",
|
||||||
perform: () => router.push("/event-types"),
|
name: "past",
|
||||||
},
|
section: "bookings",
|
||||||
{
|
shortcut: ["p", "b"],
|
||||||
id: "app-store",
|
keywords: "past bookings",
|
||||||
name: "app_store",
|
perform: () => router.push("/bookings/past"),
|
||||||
section: "apps",
|
},
|
||||||
shortcut: ["a", "s"],
|
{
|
||||||
keywords: "app store",
|
id: "cancelled-bookings",
|
||||||
perform: () => router.push("/apps"),
|
name: "cancelled",
|
||||||
},
|
section: "bookings",
|
||||||
{
|
shortcut: ["c", "b"],
|
||||||
id: "upcoming-bookings",
|
keywords: "cancelled bookings",
|
||||||
name: "upcoming",
|
perform: () => router.push("/bookings/cancelled"),
|
||||||
section: "bookings",
|
},
|
||||||
shortcut: ["u", "b"],
|
{
|
||||||
keywords: "upcoming bookings",
|
id: "schedule",
|
||||||
perform: () => router.push("/bookings/upcoming"),
|
name: "availability",
|
||||||
},
|
section: "availability",
|
||||||
{
|
shortcut: ["s", "a"],
|
||||||
id: "recurring-bookings",
|
keywords: "schedule availability",
|
||||||
name: "recurring",
|
perform: () => router.push("/availability"),
|
||||||
section: "bookings",
|
},
|
||||||
shortcut: ["r", "b"],
|
{
|
||||||
keywords: "recurring bookings",
|
id: "profile",
|
||||||
perform: () => router.push("/bookings/recurring"),
|
name: "profile",
|
||||||
},
|
section: "profile",
|
||||||
{
|
shortcut: ["p", "s"],
|
||||||
id: "past-bookings",
|
keywords: "setting profile",
|
||||||
name: "past",
|
perform: () => router.push("/settings/my-account/profile"),
|
||||||
section: "bookings",
|
},
|
||||||
shortcut: ["p", "b"],
|
{
|
||||||
keywords: "past bookings",
|
id: "avatar",
|
||||||
perform: () => router.push("/bookings/past"),
|
name: "change_avatar",
|
||||||
},
|
section: "profile",
|
||||||
{
|
shortcut: ["c", "a"],
|
||||||
id: "cancelled-bookings",
|
keywords: "remove change modify avatar",
|
||||||
name: "cancelled",
|
perform: () => router.push("/settings/my-account/profile"),
|
||||||
section: "bookings",
|
},
|
||||||
shortcut: ["c", "b"],
|
{
|
||||||
keywords: "cancelled bookings",
|
id: "timezone",
|
||||||
perform: () => router.push("/bookings/cancelled"),
|
name: "timezone",
|
||||||
},
|
section: "profile",
|
||||||
{
|
shortcut: ["c", "t"],
|
||||||
id: "schedule",
|
keywords: "change modify timezone",
|
||||||
name: "availability",
|
perform: () => router.push("/settings/my-account/general"),
|
||||||
section: "availability",
|
},
|
||||||
shortcut: ["s", "a"],
|
{
|
||||||
keywords: "schedule availability",
|
id: "brand-color",
|
||||||
perform: () => router.push("/availability"),
|
name: "brand_color",
|
||||||
},
|
section: "profile",
|
||||||
{
|
shortcut: ["b", "c"],
|
||||||
id: "profile",
|
keywords: "change modify brand color",
|
||||||
name: "profile",
|
perform: () => router.push("/settings/my-account/appearance"),
|
||||||
section: "profile",
|
},
|
||||||
shortcut: ["p", "s"],
|
{
|
||||||
keywords: "setting profile",
|
id: "teams",
|
||||||
perform: () => router.push("/settings/my-account/profile"),
|
name: "teams",
|
||||||
},
|
shortcut: ["t", "s"],
|
||||||
{
|
keywords: "add manage modify team",
|
||||||
id: "avatar",
|
perform: () => router.push("/settings/teams"),
|
||||||
name: "change_avatar",
|
},
|
||||||
section: "profile",
|
{
|
||||||
shortcut: ["c", "a"],
|
id: "password",
|
||||||
keywords: "remove change modify avatar",
|
name: "change_password",
|
||||||
perform: () => router.push("/settings/my-account/profile"),
|
section: "security",
|
||||||
},
|
shortcut: ["c", "p"],
|
||||||
{
|
keywords: "change modify password",
|
||||||
id: "timezone",
|
perform: () => router.push("/settings/security/password"),
|
||||||
name: "timezone",
|
},
|
||||||
section: "profile",
|
{
|
||||||
shortcut: ["c", "t"],
|
id: "two-factor",
|
||||||
keywords: "change modify timezone",
|
name: "two_factor_auth",
|
||||||
perform: () => router.push("/settings/my-account/general"),
|
section: "security",
|
||||||
},
|
shortcut: ["t", "f", "a"],
|
||||||
{
|
keywords: "two factor authentication",
|
||||||
id: "brand-color",
|
perform: () => router.push("/settings/security/two-factor-auth"),
|
||||||
name: "brand_color",
|
},
|
||||||
section: "profile",
|
{
|
||||||
shortcut: ["b", "c"],
|
id: "impersonation",
|
||||||
keywords: "change modify brand color",
|
name: "user_impersonation_heading",
|
||||||
perform: () => router.push("/settings/my-account/appearance"),
|
section: "security",
|
||||||
},
|
shortcut: ["u", "i"],
|
||||||
{
|
keywords: "user impersonation",
|
||||||
id: "teams",
|
perform: () => router.push("/settings/security/impersonation"),
|
||||||
name: "teams",
|
},
|
||||||
shortcut: ["t", "s"],
|
{
|
||||||
keywords: "add manage modify team",
|
id: "license",
|
||||||
perform: () => router.push("/settings/teams"),
|
name: "choose_a_license",
|
||||||
},
|
section: "admin",
|
||||||
{
|
shortcut: ["u", "l"],
|
||||||
id: "password",
|
keywords: "license",
|
||||||
name: "change_password",
|
perform: () => router.push("/auth/setup?step=1"),
|
||||||
section: "security",
|
},
|
||||||
shortcut: ["c", "p"],
|
{
|
||||||
keywords: "change modify password",
|
id: "webhooks",
|
||||||
perform: () => router.push("/settings/security/password"),
|
name: "Webhooks",
|
||||||
},
|
section: "developer",
|
||||||
{
|
shortcut: ["w", "h"],
|
||||||
id: "two-factor",
|
keywords: "webhook automation",
|
||||||
name: "two_factor_auth",
|
perform: () => router.push("/settings/developer/webhooks"),
|
||||||
section: "security",
|
},
|
||||||
shortcut: ["t", "f", "a"],
|
{
|
||||||
keywords: "two factor authentication",
|
id: "api-keys",
|
||||||
perform: () => router.push("/settings/security/two-factor-auth"),
|
name: "api_keys",
|
||||||
},
|
section: "developer",
|
||||||
{
|
shortcut: ["a", "p", "i"],
|
||||||
id: "impersonation",
|
keywords: "api keys",
|
||||||
name: "user_impersonation_heading",
|
perform: () => router.push("/settings/developer/api-keys"),
|
||||||
section: "security",
|
},
|
||||||
shortcut: ["u", "i"],
|
{
|
||||||
keywords: "user impersonation",
|
id: "billing",
|
||||||
perform: () => router.push("/settings/security/impersonation"),
|
name: "manage_billing",
|
||||||
},
|
section: "billing",
|
||||||
{
|
shortcut: ["m", "b"],
|
||||||
id: "license",
|
keywords: "billing view manage",
|
||||||
name: "choose_a_license",
|
perform: () => router.push("/settings/billing"),
|
||||||
section: "admin",
|
},
|
||||||
shortcut: ["u", "l"],
|
...appStoreActions,
|
||||||
keywords: "license",
|
];
|
||||||
perform: () => router.push("/auth/setup?step=1"),
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
},
|
}, []);
|
||||||
{
|
|
||||||
id: "webhooks",
|
|
||||||
name: "Webhooks",
|
|
||||||
section: "developer",
|
|
||||||
shortcut: ["w", "h"],
|
|
||||||
keywords: "webhook automation",
|
|
||||||
perform: () => router.push("/settings/developer/webhooks"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "api-keys",
|
|
||||||
name: "api_keys",
|
|
||||||
section: "developer",
|
|
||||||
shortcut: ["a", "p", "i"],
|
|
||||||
keywords: "api keys",
|
|
||||||
perform: () => router.push("/settings/developer/api-keys"),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "billing",
|
|
||||||
name: "manage_billing",
|
|
||||||
section: "billing",
|
|
||||||
shortcut: ["m", "b"],
|
|
||||||
keywords: "billing view manage",
|
|
||||||
perform: () => router.push("/settings/billing"),
|
|
||||||
},
|
|
||||||
...appStoreActions,
|
|
||||||
...eventTypeActions,
|
|
||||||
];
|
|
||||||
|
|
||||||
return <KBarProvider actions={actions}>{children}</KBarProvider>;
|
return <KBarProvider actions={actions}>{children}</KBarProvider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const KBarContent = () => {
|
export const KBarContent = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
useEventTypesAction();
|
||||||
return (
|
return (
|
||||||
<KBarPortal>
|
<KBarPortal>
|
||||||
<KBarPositioner>
|
<KBarPositioner>
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ const badgeStyles = cva("font-medium inline-flex items-center justify-center rou
|
|||||||
error: "bg-red-100 text-red-800",
|
error: "bg-red-100 text-red-800",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
sm: "px-1 py-0.5 text-xs",
|
sm: "px-1 py-0.5 text-xs leading-3",
|
||||||
md: "py-1 px-1.5 text-xs",
|
md: "py-1 px-1.5 text-xs leading-3",
|
||||||
lg: "py-1 px-2 text-sm",
|
lg: "py-1 px-2 text-sm leading-4",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
variant: "default",
|
variant: "default",
|
||||||
size: "sm",
|
size: "md",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export const BooleanToggleGroup = function BooleanToggleGroup({
|
|||||||
disabled = false,
|
disabled = false,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||||
onValueChange = () => {},
|
onValueChange = () => {},
|
||||||
|
...passThrough
|
||||||
}: {
|
}: {
|
||||||
defaultValue?: boolean;
|
defaultValue?: boolean;
|
||||||
value?: boolean;
|
value?: boolean;
|
||||||
@@ -44,7 +45,8 @@ export const BooleanToggleGroup = function BooleanToggleGroup({
|
|||||||
onValueChange={(yesNoValue: "yes" | "no") => {
|
onValueChange={(yesNoValue: "yes" | "no") => {
|
||||||
setYesNoValue(yesNoValue);
|
setYesNoValue(yesNoValue);
|
||||||
onValueChange(boolean(yesNoValue));
|
onValueChange(boolean(yesNoValue));
|
||||||
}}>
|
}}
|
||||||
|
{...passThrough}>
|
||||||
<ToggleGroupItemPrimitive
|
<ToggleGroupItemPrimitive
|
||||||
className={classNames(boolean(yesNoValue) ? selectedClass : unselectedClass)}
|
className={classNames(boolean(yesNoValue) ? selectedClass : unselectedClass)}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
Reference in new issue
Block a user