Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99fd4fe7a4 | ||
|
|
a677d0d2c4 | ||
|
|
d8cee20d0c | ||
|
|
a7ce12e22f | ||
|
|
88b67a8121 | ||
|
|
7e2a1a23b9 | ||
|
|
a115a3458e | ||
|
|
ae503acccc | ||
|
|
61e7c89f36 | ||
|
|
60ba7a5ca2 | ||
|
|
bfa718e193 | ||
|
|
b29a52d0b0 | ||
|
|
c21d9d08ab | ||
|
|
615e1dcfe8 | ||
|
|
a24f9b76f6 | ||
|
|
62f8ac1c6d | ||
|
|
09b91d5a7e | ||
|
|
01ac2e640a | ||
|
|
e39bf37f91 | ||
|
|
1f845944ac | ||
|
|
7f53db601d | ||
|
|
1cc9f1d25d | ||
|
|
c6ee482478 | ||
|
|
5292dd2e66 | ||
|
|
f59d62f03c | ||
|
|
cb02268e7f | ||
|
|
e33453c50d | ||
|
|
15ffeec745 | ||
|
|
b7252ed79c | ||
|
|
9b04df9bbe | ||
|
|
657be16a4e | ||
|
|
b0afb15939 | ||
|
|
3a9b8a9bf6 | ||
|
|
92a919069d | ||
|
|
0071d16575 | ||
|
|
23d22ee7ea | ||
|
|
3a4f944aac | ||
|
|
3eadae0aad | ||
|
|
0cc2fe2d7f | ||
|
|
0970c77009 | ||
|
|
5b733765f4 | ||
|
|
83fe4d6625 | ||
|
|
91beeeb525 | ||
|
|
81c0b1ffe9 | ||
|
|
aedc88f975 | ||
|
|
67256b5b6e | ||
|
|
ef02d64f90 | ||
|
|
8685862617 | ||
|
|
7422488b5d | ||
|
|
c3e1481d6a | ||
|
|
a1d1883b34 | ||
|
|
f2ae94a190 | ||
|
|
b82a597645 | ||
|
|
5ba0ee8e85 | ||
|
|
1463f97f14 | ||
|
|
34fea35e83 | ||
|
|
2d75048037 | ||
|
|
b549735334 | ||
|
|
86c379817e | ||
|
|
5d1976295c | ||
|
|
e9b1fa1f50 | ||
|
|
80080dda7a | ||
|
|
27c25e1f95 | ||
|
|
858be0f5d8 | ||
|
|
68d0090f50 | ||
|
|
a43fa13b11 | ||
|
|
2bf6da30b3 | ||
|
|
d85e0b51e4 | ||
|
|
55a317d677 | ||
|
|
5fff8fc58c | ||
|
|
0d68767d0a | ||
|
|
2051e87436 | ||
|
|
77a157a12b | ||
|
|
c4f25f2c44 | ||
|
|
d967facb63 | ||
|
|
c6dad39fb0 | ||
|
|
eec7f4d7d4 | ||
|
|
95d5824ba9 | ||
|
|
5f95a4d041 | ||
|
|
8e97444129 | ||
|
|
8d8264b5f5 | ||
|
|
2ad81221d0 | ||
|
|
3d1ac4523f | ||
|
|
b38807cb0f | ||
|
|
11fb387370 | ||
|
|
1546f9115d | ||
|
|
40ecd5dd9d | ||
|
|
ac6ac14a23 | ||
|
|
c6534160e4 | ||
|
|
aa2db562b8 | ||
|
|
b7ed314be2 | ||
|
|
d102268b33 | ||
|
|
f4397d6593 | ||
|
|
192603a1fa | ||
|
|
a3d969680f | ||
|
|
adc3b3aec6 | ||
|
|
92286e2453 | ||
|
|
51b2a15cb0 | ||
|
|
fa9a099dd6 | ||
|
|
83ceb46cca | ||
|
|
75d5576e17 | ||
|
|
0a6dde4fcc | ||
|
|
73b5839284 | ||
|
|
1a42b94b13 | ||
|
|
acb910f680 | ||
|
|
38e857f9a9 | ||
|
|
b8149dee36 | ||
|
|
f583ef40f2 | ||
|
|
71574488c5 | ||
|
|
008e0e43a2 | ||
|
|
da22eda662 | ||
|
|
f94d696885 | ||
|
|
9e84eb1b2c | ||
|
|
4c549811a1 | ||
|
|
05502f0cfb | ||
|
|
e436b8e06b | ||
|
|
8146cf8c93 | ||
|
|
c05f595ef0 | ||
|
|
0a795f5363 | ||
|
|
5b50a469b2 | ||
|
|
c08365f09c | ||
|
|
477e102388 | ||
|
|
e1b10b4ec5 | ||
|
|
ff6db3c572 | ||
|
|
4fa3fdef97 | ||
|
|
f29f6632e8 | ||
|
|
36747988e6 | ||
|
|
3dd312b88b | ||
|
|
3c9e9329b0 | ||
|
|
8617f43a8e | ||
|
|
5f0eb652b0 | ||
|
|
26d7301885 | ||
|
|
ffc4d22ff1 | ||
|
|
ee286a600f | ||
|
|
72c493ee21 | ||
|
|
5bd0428d18 | ||
|
|
c7089b9db6 | ||
|
|
81ad00a816 | ||
|
|
b5730da56d | ||
|
|
9455d14ad5 | ||
|
|
a056160404 | ||
|
|
6d53cd84f8 | ||
|
|
30cb8f745d | ||
|
|
bafa3c97ac |
+2
-2
@@ -294,9 +294,9 @@ E2E_TEST_CALCOM_GCAL_KEYS=
|
||||
# Used for self-hosters that are implementing Cal.com into their applications that already have certain integrations
|
||||
# Under settings/admin/apps ensure that all app secrets are set the same as the parent application
|
||||
# You can use: `openssl rand -base64 32` to generate one
|
||||
CALCOM_WEBHOOK_SECRET=""
|
||||
CALCOM_CREDENTIAL_SYNC_SECRET=""
|
||||
# This is the header name that will be used to verify the webhook secret. Should be in lowercase
|
||||
CALCOM_WEBHOOK_HEADER_NAME="calcom-webhook-secret"
|
||||
CALCOM_CREDENTIAL_SYNC_SECRET_HEADER_NAME="calcom-webhook-secret"
|
||||
CALCOM_CREDENTIAL_SYNC_ENDPOINT=""
|
||||
# Key should match on Cal.com and your application
|
||||
# must be 24 bytes for AES256 encryption algorithm
|
||||
|
||||
+63
-57
@@ -1,5 +1,6 @@
|
||||
import { ChatPromptTemplate } from "@langchain/core/prompts";
|
||||
import { ChatOpenAI } from "@langchain/openai";
|
||||
import { initializeAgentExecutorWithOptions } from "langchain/agents";
|
||||
import { AgentExecutor, createOpenAIFunctionsAgent } from "langchain/agents";
|
||||
|
||||
import { env } from "../env.mjs";
|
||||
import createBookingIfAvailable from "../tools/createBooking";
|
||||
@@ -47,67 +48,72 @@ const agent = async (
|
||||
/**
|
||||
* Initialize the agent executor with arguments.
|
||||
*/
|
||||
const executor = await initializeAgentExecutorWithOptions(tools, model, {
|
||||
agentArgs: {
|
||||
prefix: `You are Cal.ai - a bleeding edge scheduling assistant that interfaces via email.
|
||||
Make sure your final answers are definitive, complete and well formatted.
|
||||
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
|
||||
Tools will always handle times in UTC, but times sent to users should be formatted per that user's timezone.
|
||||
In responses to users, always summarize necessary context and open the door to follow ups. For example "I have booked your chat with @username for 3pm on Wednesday, December 20th, 2023 EST. Please let me know if you need to reschedule."
|
||||
If you can't find a referenced user, ask the user for their email or @username. Make sure to specify that usernames require the @username format. Users don't know other users' userIds.
|
||||
const prompt =
|
||||
ChatPromptTemplate.fromTemplate(`You are Cal.ai - a bleeding edge scheduling assistant that interfaces via email.
|
||||
Make sure your final answers are definitive, complete and well formatted.
|
||||
Sometimes, tools return errors. In this case, try to handle the error intelligently or ask the user for more information.
|
||||
Tools will always handle times in UTC, but times sent to users should be formatted per that user's timezone.
|
||||
In responses to users, always summarize necessary context and open the door to follow ups. For example "I have booked your chat with @username for 3pm on Wednesday, December 20th, 2023 EST. Please let me know if you need to reschedule."
|
||||
If you can't find a referenced user, ask the user for their email or @username. Make sure to specify that usernames require the @username format. Users don't know other users' userIds.
|
||||
|
||||
The primary user's id is: ${userId}
|
||||
The primary user's username is: ${user.username}
|
||||
The current time in the primary user's timezone is: ${now(user.timeZone, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
The primary user's time zone is: ${user.timeZone}
|
||||
The primary user's event types are: ${user.eventTypes
|
||||
.map((e: EventType) => `ID: ${e.id}, Slug: ${e.slug}, Title: ${e.title}, Length: ${e.length};`)
|
||||
.join("\n")}
|
||||
The primary user's working hours are: ${user.workingHours
|
||||
.map(
|
||||
(w: WorkingHours) =>
|
||||
`Days: ${w.days.join(", ")}, Start Time (minutes in UTC): ${
|
||||
w.startTime
|
||||
}, End Time (minutes in UTC): ${w.endTime};`
|
||||
)
|
||||
.join("\n")}
|
||||
${
|
||||
users.length
|
||||
? `The email references the following @usernames and emails: ${users
|
||||
.map(
|
||||
(u) =>
|
||||
`${
|
||||
(u.id ? `, id: ${u.id}` : "id: (non user)") +
|
||||
(u.username
|
||||
? u.type === "fromUsername"
|
||||
? `, username: @${u.username}`
|
||||
: ", username: REDACTED"
|
||||
: ", (no username)") +
|
||||
(u.email
|
||||
? u.type === "fromEmail"
|
||||
? `, email: ${u.email}`
|
||||
: ", email: REDACTED"
|
||||
: ", (no email)")
|
||||
};`
|
||||
)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
}`);
|
||||
const agent = await createOpenAIFunctionsAgent({
|
||||
llm: model,
|
||||
prompt,
|
||||
tools,
|
||||
});
|
||||
|
||||
The primary user's id is: ${userId}
|
||||
The primary user's username is: ${user.username}
|
||||
The current time in the primary user's timezone is: ${now(user.timeZone, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
})}
|
||||
The primary user's time zone is: ${user.timeZone}
|
||||
The primary user's event types are: ${user.eventTypes
|
||||
.map((e: EventType) => `ID: ${e.id}, Slug: ${e.slug}, Title: ${e.title}, Length: ${e.length};`)
|
||||
.join("\n")}
|
||||
The primary user's working hours are: ${user.workingHours
|
||||
.map(
|
||||
(w: WorkingHours) =>
|
||||
`Days: ${w.days.join(", ")}, Start Time (minutes in UTC): ${
|
||||
w.startTime
|
||||
}, End Time (minutes in UTC): ${w.endTime};`
|
||||
)
|
||||
.join("\n")}
|
||||
${
|
||||
users.length
|
||||
? `The email references the following @usernames and emails: ${users
|
||||
.map(
|
||||
(u) =>
|
||||
`${
|
||||
(u.id ? `, id: ${u.id}` : "id: (non user)") +
|
||||
(u.username
|
||||
? u.type === "fromUsername"
|
||||
? `, username: @${u.username}`
|
||||
: ", username: REDACTED"
|
||||
: ", (no username)") +
|
||||
(u.email
|
||||
? u.type === "fromEmail"
|
||||
? `, email: ${u.email}`
|
||||
: ", email: REDACTED"
|
||||
: ", (no email)")
|
||||
};`
|
||||
)
|
||||
.join("\n")}`
|
||||
: ""
|
||||
}
|
||||
`,
|
||||
},
|
||||
agentType: "openai-functions",
|
||||
const executor = new AgentExecutor({
|
||||
agent,
|
||||
tools,
|
||||
returnIntermediateSteps: env.NODE_ENV === "development",
|
||||
verbose: env.NODE_ENV === "development",
|
||||
});
|
||||
|
||||
const result = await executor.call({ input });
|
||||
const result = await executor.invoke({ input });
|
||||
const { output } = result;
|
||||
|
||||
return output;
|
||||
|
||||
@@ -74,6 +74,7 @@ export const schemaUserBaseBodyParams = User.pick({
|
||||
timeZone: true,
|
||||
weekStart: true,
|
||||
theme: true,
|
||||
appTheme: true,
|
||||
defaultScheduleId: true,
|
||||
locale: true,
|
||||
hideBranding: true,
|
||||
@@ -100,6 +101,7 @@ const schemaUserEditParams = z.object({
|
||||
hideBranding: z.boolean().optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
theme: z.nativeEnum(theme).optional().nullable(),
|
||||
appTheme: z.nativeEnum(theme).optional().nullable(),
|
||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||
defaultScheduleId: z
|
||||
.number()
|
||||
@@ -122,6 +124,7 @@ const schemaUserCreateParams = z.object({
|
||||
hideBranding: z.boolean().optional(),
|
||||
timeZone: timeZone.optional(),
|
||||
theme: z.nativeEnum(theme).optional().nullable(),
|
||||
appTheme: z.nativeEnum(theme).optional().nullable(),
|
||||
timeFormat: z.nativeEnum(timeFormat).optional(),
|
||||
defaultScheduleId: z
|
||||
.number()
|
||||
@@ -159,6 +162,7 @@ export const schemaUserReadPublic = User.pick({
|
||||
weekStart: true,
|
||||
endTime: true,
|
||||
bufferTime: true,
|
||||
appTheme: true,
|
||||
theme: true,
|
||||
defaultScheduleId: true,
|
||||
locale: true,
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "18.0.26",
|
||||
"@types/react-dom": "^18.0.9",
|
||||
"@types/swagger-ui-react": "^4.18.3",
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import "highlight.js/styles/default.css";
|
||||
import { type AppProps } from "next/app";
|
||||
import "swagger-ui-react/swagger-ui.css";
|
||||
|
||||
import "../styles/globals.css";
|
||||
|
||||
function MyApp({ Component, pageProps }) {
|
||||
function MyApp({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import dynamic from "next/dynamic";
|
||||
import type { SwaggerUI } from "swagger-ui-react";
|
||||
|
||||
import { SnippedGenerator, requestSnippets } from "@lib/snippets";
|
||||
|
||||
const SwaggerUIDynamic: SwaggerUI & { url: string } = dynamic(() => import("swagger-ui-react"), {
|
||||
const SwaggerUIDynamic = dynamic(() => import("swagger-ui-react"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
@@ -17,8 +16,6 @@ export default function APIDocs() {
|
||||
requestSnippets={requestSnippets}
|
||||
plugins={[SnippedGenerator]}
|
||||
tryItOutEnabled={true}
|
||||
syntaxHighlight={true}
|
||||
enableCORS={false} // Doesn't seem to work either
|
||||
docExpansion="list"
|
||||
filter={true}
|
||||
/>
|
||||
|
||||
@@ -18,6 +18,7 @@ const calFont = localFont({
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "block",
|
||||
weight: "600",
|
||||
});
|
||||
|
||||
export const generateMetadata = () =>
|
||||
|
||||
@@ -24,6 +24,7 @@ const calFont = localFont({
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "swap",
|
||||
weight: "600",
|
||||
});
|
||||
|
||||
function PageWrapper(props: AppProps) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
@@ -23,13 +24,22 @@ type Props = {
|
||||
theme: string | null;
|
||||
allRemainingBookings: boolean;
|
||||
seatReferenceUid?: string;
|
||||
bookingCancelledEventProps: {
|
||||
booking: unknown;
|
||||
organizer: {
|
||||
name: string;
|
||||
email: string;
|
||||
timeZone?: string;
|
||||
};
|
||||
eventType: unknown;
|
||||
};
|
||||
};
|
||||
|
||||
export default function CancelBooking(props: Props) {
|
||||
const [cancellationReason, setCancellationReason] = useState<string>("");
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { booking, allRemainingBookings, seatReferenceUid } = props;
|
||||
const { booking, allRemainingBookings, seatReferenceUid, bookingCancelledEventProps } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const telemetry = useTelemetry();
|
||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||
@@ -97,8 +107,17 @@ export default function CancelBooking(props: Props) {
|
||||
method: "POST",
|
||||
});
|
||||
|
||||
const bookingWithCancellationReason = {
|
||||
...(bookingCancelledEventProps.booking as object),
|
||||
cancellationReason,
|
||||
} as unknown;
|
||||
|
||||
if (res.status >= 200 && res.status < 300) {
|
||||
// tested by apps/web/playwright/booking-pages.e2e.ts
|
||||
sdkActionManager?.fire("bookingCancelled", {
|
||||
...bookingCancelledEventProps,
|
||||
booking: bookingWithCancellationReason,
|
||||
});
|
||||
router.refresh();
|
||||
} else {
|
||||
setLoading(false);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
@@ -26,8 +27,10 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Badge,
|
||||
CheckboxField,
|
||||
Label,
|
||||
SelectField,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
Switch,
|
||||
@@ -59,7 +62,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const selectedThemeIsDark =
|
||||
user?.theme === "dark" ||
|
||||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
||||
|
||||
formMethods.getValues().bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = `${name} input`;
|
||||
});
|
||||
@@ -101,7 +103,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
};
|
||||
}
|
||||
return field;
|
||||
})
|
||||
}),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
@@ -121,6 +124,16 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
const displayDestinationCalendarSelector =
|
||||
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
|
||||
const verifiedSecondaryEmails = [
|
||||
{
|
||||
label: user?.email || "",
|
||||
value: -1,
|
||||
},
|
||||
...(user?.secondaryEmails || [])
|
||||
.filter((secondaryEmail) => !!secondaryEmail.emailVerified)
|
||||
.map((secondaryEmail) => ({ label: secondaryEmail.email, value: secondaryEmail.id })),
|
||||
];
|
||||
const selectedSecondaryEmailId = formMethods.getValues("secondaryEmailId") || -1;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-4">
|
||||
@@ -179,15 +192,46 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
{displayDestinationCalendarSelector && (
|
||||
<div className="w-full">
|
||||
<Switch
|
||||
label={t("display_add_to_calendar_organizer")}
|
||||
tooltip={t("reconnect_calendar_to_use")}
|
||||
label={
|
||||
<>
|
||||
{t("display_add_to_calendar_organizer")}
|
||||
<InfoIcon className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md" />
|
||||
</>
|
||||
}
|
||||
checked={useEventTypeDestinationCalendarEmail}
|
||||
onCheckedChange={(val) => {
|
||||
setUseEventTypeDestinationCalendarEmail(val);
|
||||
formMethods.setValue("useEventTypeDestinationCalendarEmail", val);
|
||||
formMethods.setValue("useEventTypeDestinationCalendarEmail", val, { shouldDirty: true });
|
||||
if (val) {
|
||||
showToast(t("reconnect_calendar_to_use"), "warning");
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!useEventTypeDestinationCalendarEmail && verifiedSecondaryEmails.length > 0 && (
|
||||
<div className="w-full">
|
||||
<SelectField
|
||||
label={t("send_event_details_to")}
|
||||
placeholder={
|
||||
selectedSecondaryEmailId === -1 && (
|
||||
<span className="text-default min-w-0 overflow-hidden truncate whitespace-nowrap">
|
||||
<Badge variant="blue">Default</Badge> {user?.email || ""}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
onChange={(option) =>
|
||||
formMethods.setValue("secondaryEmailId", option?.value, { shouldDirty: true })
|
||||
}
|
||||
value={verifiedSecondaryEmails.find(
|
||||
(secondaryEmail) =>
|
||||
selectedSecondaryEmailId !== -1 && secondaryEmail.value === selectedSecondaryEmailId
|
||||
)}
|
||||
options={verifiedSecondaryEmails}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<BookerLayoutSelector fallbackToUserSettings isDark={selectedThemeIsDark} isOuterBorder={true} />
|
||||
@@ -319,7 +363,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
description={t("private_link_description", { appName: APP_NAME })}
|
||||
checked={hashedLinkVisible}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true });
|
||||
setHashedLinkVisible(e);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -383,10 +427,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||
if (e) {
|
||||
toggleGuests(false);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
formMethods.setValue("requiresConfirmation", false, { shouldDirty: true });
|
||||
setRequiresConfirmation(false);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
toggleGuests(true);
|
||||
@@ -506,7 +552,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
{showEventNameTip && (
|
||||
<CustomEventTypeModal
|
||||
close={closeEventNameTip}
|
||||
setValue={(val: string) => formMethods.setValue("eventName", val)}
|
||||
setValue={(val: string) => formMethods.setValue("eventName", val, { shouldDirty: true })}
|
||||
defaultValue={formMethods.getValues("eventName")}
|
||||
placeHolder={eventNamePlaceholder}
|
||||
event={eventNameObject}
|
||||
|
||||
@@ -31,10 +31,14 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
const allAppsData = formMethods.watch("metadata")?.apps || {};
|
||||
|
||||
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
||||
formMethods.setValue("metadata", {
|
||||
...formMethods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata",
|
||||
{
|
||||
...formMethods.getValues("metadata"),
|
||||
apps: _allAppsData,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
const getAppDataGetter = (appId: EventTypeAppsList): GetAppData => {
|
||||
|
||||
@@ -230,7 +230,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||
);
|
||||
|
||||
setValue("availability", value);
|
||||
setValue("availability", value, { shouldDirty: true });
|
||||
},
|
||||
[data]
|
||||
);
|
||||
@@ -238,7 +238,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!availabilityValue?.value) return;
|
||||
setValue("schedule", availabilityValue.value);
|
||||
setValue("schedule", availabilityValue.value, { shouldDirty: true });
|
||||
}, [availabilityValue, setValue]);
|
||||
|
||||
return (
|
||||
@@ -260,7 +260,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
isSearchable={false}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
if (selected?.value) setValue("availability", selected);
|
||||
if (selected?.value) setValue("availability", selected, { shouldDirty: true });
|
||||
}}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||
value={availabilityValue}
|
||||
@@ -298,7 +298,7 @@ const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSe
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(!checked);
|
||||
if (!checked) {
|
||||
setValue("schedule", null);
|
||||
setValue("schedule", null, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
|
||||
@@ -61,7 +61,8 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
minimumBookingNoticeDisplayValues.type,
|
||||
"minutes",
|
||||
minimumBookingNoticeDisplayValues.value
|
||||
)
|
||||
),
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
|
||||
|
||||
@@ -230,7 +231,9 @@ export const EventLimitsTab = () => {
|
||||
<Select
|
||||
isSearchable={false}
|
||||
onChange={(val) => {
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
defaultValue={
|
||||
slotIntervalOptions.find(
|
||||
@@ -258,11 +261,15 @@ export const EventLimitsTab = () => {
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("bookingLimits", {
|
||||
PER_DAY: 1,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"bookingLimits",
|
||||
{
|
||||
PER_DAY: 1,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
} else {
|
||||
formMethods.setValue("bookingLimits", {});
|
||||
formMethods.setValue("bookingLimits", {}, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
@@ -279,7 +286,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="onlyShowFirstAvailableSlot"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -289,7 +296,7 @@ export const EventLimitsTab = () => {
|
||||
description={t("limit_booking_only_first_slot_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
formMethods.setValue("onlyShowFirstAvailableSlot", active ?? false);
|
||||
onChange(active ?? false);
|
||||
}}
|
||||
switchContainerClassName={classNames(
|
||||
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
|
||||
@@ -301,7 +308,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="durationLimits"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = Object.keys(value ?? {}).length > 0;
|
||||
return (
|
||||
<SettingsToggle
|
||||
@@ -317,11 +324,11 @@ export const EventLimitsTab = () => {
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
formMethods.setValue("durationLimits", {
|
||||
onChange({
|
||||
PER_DAY: 60,
|
||||
});
|
||||
} else {
|
||||
formMethods.setValue("durationLimits", {});
|
||||
onChange({});
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -338,7 +345,7 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
<Controller
|
||||
name="periodType"
|
||||
render={({ field: { value } }) => {
|
||||
render={({ field: { onChange, value } }) => {
|
||||
const isChecked = value && value !== "UNLIMITED";
|
||||
|
||||
return (
|
||||
@@ -353,11 +360,18 @@ export const EventLimitsTab = () => {
|
||||
title={t("limit_future_bookings")}
|
||||
description={t("limit_future_bookings_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||
onCheckedChange={(bool) => {
|
||||
if (bool && !formMethods.getValues("periodDays")) {
|
||||
formMethods.setValue("periodDays", 30, { shouldDirty: true });
|
||||
}
|
||||
return onChange(bool ? "ROLLING" : "UNLIMITED");
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<RadioGroup.Root
|
||||
value={watchPeriodType}
|
||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||
onValueChange={(val) =>
|
||||
formMethods.setValue("periodType", val as PeriodType, { shouldDirty: true })
|
||||
}>
|
||||
{PERIOD_TYPES.map((period) => {
|
||||
if (period.type === "UNLIMITED") return null;
|
||||
return (
|
||||
@@ -388,17 +402,21 @@ export const EventLimitsTab = () => {
|
||||
options={optionsPeriod}
|
||||
isSearchable={false}
|
||||
onChange={(opt) =>
|
||||
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
|
||||
formMethods.setValue(
|
||||
"periodCountCalendarDays",
|
||||
opt?.value === 1 ? true : false,
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
name="periodCoundCalendarDays"
|
||||
value={optionsPeriod.find((opt) => {
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0);
|
||||
})}
|
||||
defaultValue={optionsPeriod.find(
|
||||
(opt) =>
|
||||
opt.value ===
|
||||
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
|
||||
(formMethods.getValues("periodCountCalendarDays") === true ? 1 : 0)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
@@ -407,12 +425,12 @@ export const EventLimitsTab = () => {
|
||||
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||
<Controller
|
||||
name="periodDates"
|
||||
render={() => (
|
||||
render={({ field: { onChange } }) => (
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
formMethods.setValue("periodDates", {
|
||||
onChange({
|
||||
startDate,
|
||||
endDate,
|
||||
});
|
||||
@@ -446,7 +464,7 @@ export const EventLimitsTab = () => {
|
||||
onCheckedChange={(active) => {
|
||||
setOffsetToggle(active);
|
||||
if (!active) {
|
||||
formMethods.setValue("offsetStart", 0);
|
||||
formMethods.setValue("offsetStart", 0, { shouldDirty: true });
|
||||
}
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -577,11 +595,16 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
);
|
||||
if (!rest || !currentKeys.length) return;
|
||||
//currentDurationLimits is always defined so can be casted
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(propertyName, {
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
});
|
||||
|
||||
setValue(
|
||||
propertyName,
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
{
|
||||
...watchIntervalLimits,
|
||||
[rest.value]: defaultLimit,
|
||||
},
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -611,7 +634,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
)}
|
||||
onLimitChange={(intervalLimitKey, val) =>
|
||||
// @ts-expect-error FIXME Fix these typings
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val)
|
||||
setValue(`${propertyName}.${intervalLimitKey}`, val, { shouldDirty: true })
|
||||
}
|
||||
onDelete={(intervalLimitKey) => {
|
||||
const current = currentIntervalLimits;
|
||||
|
||||
@@ -83,7 +83,7 @@ const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
|
||||
return mounted ? (
|
||||
<Editor
|
||||
getText={() => md.render(formMethods.getValues("description") || "")}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value), { shouldDirty: true })}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
editable={isEditable}
|
||||
@@ -318,7 +318,7 @@ export const EventSetupTab = (
|
||||
/>
|
||||
</div>
|
||||
<ErrorMessage
|
||||
errors={formMethods.formState.errors.locations?.[index]}
|
||||
errors={formMethods.formState.errors?.locations?.[index]}
|
||||
name={eventLocationType.defaultValueVariable}
|
||||
className="text-error my-1 ml-6 text-sm"
|
||||
as="div"
|
||||
@@ -512,16 +512,16 @@ export const EventSetupTab = (
|
||||
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
|
||||
if (newOptions.length > 0) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
|
||||
} else {
|
||||
setDefaultDuration(null);
|
||||
}
|
||||
}
|
||||
if (newOptions.length === 1 && defaultDuration === null) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
formMethods.setValue("length", newOptions[0].value, { shouldDirty: true });
|
||||
}
|
||||
formMethods.setValue("metadata.multipleDuration", values);
|
||||
formMethods.setValue("metadata.multipleDuration", values, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -542,7 +542,7 @@ export const EventSetupTab = (
|
||||
setDefaultDuration(
|
||||
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||
);
|
||||
if (option) formMethods.setValue("length", option.value);
|
||||
if (option) formMethods.setValue("length", option.value, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -571,12 +571,12 @@ export const EventSetupTab = (
|
||||
setMultipleDuration(undefined);
|
||||
setSelectedMultipleDuration([]);
|
||||
setDefaultDuration(null);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("length", eventType.length);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined, { shouldDirty: true });
|
||||
formMethods.setValue("length", eventType.length, { shouldDirty: true });
|
||||
} else {
|
||||
setMultipleDuration([]);
|
||||
formMethods.setValue("metadata.multipleDuration", []);
|
||||
formMethods.setValue("length", 0);
|
||||
formMethods.setValue("metadata.multipleDuration", [], { shouldDirty: true });
|
||||
formMethods.setValue("length", 0, { shouldDirty: true });
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -127,7 +127,7 @@ const AssignAllTeamMembers = ({
|
||||
labelClassName="mt-0.5 font-normal"
|
||||
checked={assignAllTeamMembers}
|
||||
onCheckedChange={(active) => {
|
||||
setValue("assignAllTeamMembers", active);
|
||||
setValue("assignAllTeamMembers", active, { shouldDirty: true });
|
||||
setAssignAllTeamMembers(active);
|
||||
if (active) {
|
||||
onActive();
|
||||
@@ -250,7 +250,8 @@ const FixedHosts = ({
|
||||
isFixed: true,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
})),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -269,7 +270,7 @@ const FixedHosts = ({
|
||||
const rrHosts = getValues("hosts")
|
||||
.filter((host) => !host.isFixed)
|
||||
.sort((a, b) => (b.priority ?? 2) - (a.priority ?? 2));
|
||||
setValue("hosts", rrHosts);
|
||||
setValue("hosts", rrHosts, { shouldDirty: true });
|
||||
}
|
||||
setIsDisabled(checked);
|
||||
}}
|
||||
@@ -290,7 +291,8 @@ const FixedHosts = ({
|
||||
isFixed: true,
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
})),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -332,7 +334,7 @@ const AddMembersWithSwitch = ({
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={onActive}
|
||||
onInactive={() => setValue("hosts", [])}
|
||||
onInactive={() => setValue("hosts", [], { shouldDirty: true })}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
@@ -395,7 +397,8 @@ const RoundRobinHosts = ({
|
||||
userId: parseInt(teamMember.value, 10),
|
||||
priority: 2,
|
||||
}))
|
||||
.sort((a, b) => b.priority - a.priority)
|
||||
.sort((a, b) => b.priority - a.priority),
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -420,7 +423,7 @@ const ChildrenEventTypes = ({
|
||||
<AssignAllTeamMembers
|
||||
assignAllTeamMembers={assignAllTeamMembers}
|
||||
setAssignAllTeamMembers={setAssignAllTeamMembers}
|
||||
onActive={() => setValue("children", childrenEventTypeOptions)}
|
||||
onActive={() => setValue("children", childrenEventTypeOptions, { shouldDirty: true })}
|
||||
/>
|
||||
{!assignAllTeamMembers ? (
|
||||
<Controller<FormValues>
|
||||
@@ -449,7 +452,7 @@ const Hosts = ({
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
control,
|
||||
resetField,
|
||||
setValue,
|
||||
getValues,
|
||||
formState: { submitCount },
|
||||
} = useFormContext<FormValues>();
|
||||
@@ -469,10 +472,12 @@ const Hosts = ({
|
||||
initialValue.current = { hosts: getValues("hosts"), schedulingType, submitCount };
|
||||
return;
|
||||
}
|
||||
resetField("hosts", {
|
||||
defaultValue: initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
});
|
||||
}, [schedulingType, resetField, getValues, submitCount]);
|
||||
setValue(
|
||||
"hosts",
|
||||
initialValue.current.schedulingType === schedulingType ? initialValue.current.hosts : [],
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}, [schedulingType, setValue, getValues, submitCount]);
|
||||
|
||||
return (
|
||||
<Controller<FormValues>
|
||||
@@ -586,7 +591,7 @@ export const EventTeamTab = ({
|
||||
className="w-full"
|
||||
onChange={(val) => {
|
||||
onChange(val?.value);
|
||||
setValue("assignAllTeamMembers", false);
|
||||
setValue("assignAllTeamMembers", false, { shouldDirty: true });
|
||||
setAssignAllTeamMembers(false);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -14,6 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { trpc, TRPCClientError } from "@calcom/trpc/react";
|
||||
import type { DialogProps } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
ButtonGroup,
|
||||
@@ -133,6 +134,60 @@ function getNavigation({
|
||||
];
|
||||
}
|
||||
|
||||
function DeleteDialog({
|
||||
isManagedEvent,
|
||||
eventTypeId,
|
||||
open,
|
||||
onOpenChange,
|
||||
}: { isManagedEvent: string; eventTypeId: number } & Pick<DialogProps, "open" | "onOpenChange">) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
router.push("/event-types");
|
||||
onOpenChange?.(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
onOpenChange?.(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: eventTypeId });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>Anyone who they've shared their link with will no longer be able to book using it.</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function EventTypeSingleLayout({
|
||||
children,
|
||||
eventType,
|
||||
@@ -149,9 +204,7 @@ function EventTypeSingleLayout({
|
||||
bookerUrl,
|
||||
activeWebhooksNumber,
|
||||
}: Props) {
|
||||
const utils = trpc.useContext();
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const hasPermsToDelete =
|
||||
@@ -160,24 +213,6 @@ function EventTypeSingleLayout({
|
||||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
|
||||
isUserOrganizationAdmin;
|
||||
|
||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||
onSuccess: async () => {
|
||||
await utils.viewer.eventTypes.invalidate();
|
||||
showToast(t("event_type_deleted_successfully"), "success");
|
||||
router.push("/event-types");
|
||||
setDeleteDialogOpen(false);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
setDeleteDialogOpen(false);
|
||||
} else if (err instanceof TRPCClientError) {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
@@ -308,7 +343,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -424,7 +459,7 @@ function EventTypeSingleLayout({
|
||||
id="hiddenSwitch"
|
||||
checked={!formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", !e);
|
||||
formMethods.setValue("hidden", !e, { shouldDirty: true });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -435,6 +470,7 @@ function EventTypeSingleLayout({
|
||||
className="ml-4 lg:ml-0"
|
||||
type="submit"
|
||||
loading={isUpdateMutationLoading}
|
||||
disabled={!formMethods.formState.isDirty}
|
||||
data-testid="update-eventtype"
|
||||
form="event-type-form">
|
||||
{t("save")}
|
||||
@@ -467,31 +503,13 @@ function EventTypeSingleLayout({
|
||||
</div>
|
||||
</div>
|
||||
</Suspense>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
isPending={deleteMutation.isPending}
|
||||
variety="danger"
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
deleteMutation.mutate({ id: formMethods.getValues("id") });
|
||||
}}>
|
||||
<p className="mt-5">
|
||||
<Trans
|
||||
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||
<ul>
|
||||
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||
<li>
|
||||
Anyone who they've shared their link with will no longer be able to book using it.
|
||||
</li>
|
||||
</ul>
|
||||
</Trans>
|
||||
</p>
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
<DeleteDialog
|
||||
eventTypeId={eventType.id}
|
||||
isManagedEvent={isManagedEvent}
|
||||
open={deleteDialogOpen}
|
||||
onOpenChange={setDeleteDialogOpen}
|
||||
/>
|
||||
|
||||
<EventTypeEmbedDialog />
|
||||
</Shell>
|
||||
);
|
||||
|
||||
@@ -81,10 +81,10 @@ export default function InstantEventController({
|
||||
data-testid="instant-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("isInstantEvent", false);
|
||||
formMethods.setValue("isInstantEvent", false, { shouldDirty: true });
|
||||
setInstantEventState(false);
|
||||
} else {
|
||||
formMethods.setValue("isInstantEvent", true);
|
||||
formMethods.setValue("isInstantEvent", true, { shouldDirty: true });
|
||||
setInstantEventState(true);
|
||||
}
|
||||
}}>
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function RecurringEventController({
|
||||
data-testid="recurring-event-check"
|
||||
onCheckedChange={(e) => {
|
||||
if (!e) {
|
||||
formMethods.setValue("recurringEvent", null);
|
||||
formMethods.setValue("recurringEvent", null, { shouldDirty: true });
|
||||
setRecurringEventState(null);
|
||||
} else {
|
||||
const newVal = eventType.recurringEvent || {
|
||||
@@ -74,7 +74,7 @@ export default function RecurringEventController({
|
||||
count: 12,
|
||||
freq: Frequency.WEEKLY,
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}
|
||||
}}>
|
||||
@@ -95,7 +95,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
interval: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
@@ -110,7 +110,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
@@ -129,7 +129,7 @@ export default function RecurringEventController({
|
||||
...recurringEventState,
|
||||
count: parseInt(event?.target.value),
|
||||
};
|
||||
formMethods.setValue("recurringEvent", newVal);
|
||||
formMethods.setValue("recurringEvent", newVal, { shouldDirty: true });
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function RequiresConfirmationController({
|
||||
|
||||
useEffect(() => {
|
||||
if (!requiresConfirmation) {
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, { shouldDirty: true });
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [requiresConfirmation]);
|
||||
@@ -82,7 +82,7 @@ export default function RequiresConfirmationController({
|
||||
checked={requiresConfirmation}
|
||||
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
|
||||
onCheckedChange={(val) => {
|
||||
formMethods.setValue("requiresConfirmation", val);
|
||||
formMethods.setValue("requiresConfirmation", val, { shouldDirty: true });
|
||||
onRequiresConfirmation(val);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
@@ -96,16 +96,19 @@ export default function RequiresConfirmationController({
|
||||
}
|
||||
onValueChange={(val) => {
|
||||
if (val === "always") {
|
||||
formMethods.setValue("requiresConfirmation", true);
|
||||
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
|
||||
onRequiresConfirmation(true);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined);
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold", undefined, {
|
||||
shouldDirty: true,
|
||||
});
|
||||
setRequiresConfirmationSetup(undefined);
|
||||
} else if (val === "notice") {
|
||||
formMethods.setValue("requiresConfirmation", true);
|
||||
formMethods.setValue("requiresConfirmation", true, { shouldDirty: true });
|
||||
onRequiresConfirmation(true);
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold",
|
||||
requiresConfirmationSetup || defaultRequiresConfirmationSetup
|
||||
requiresConfirmationSetup || defaultRequiresConfirmationSetup,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}
|
||||
}}>
|
||||
@@ -146,7 +149,8 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.time",
|
||||
val
|
||||
val,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
className="border-default !m-0 block w-16 rounded-r-none border-r-0 text-sm [appearance:textfield] focus:z-10 focus:border-r"
|
||||
@@ -171,7 +175,8 @@ export default function RequiresConfirmationController({
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.unit",
|
||||
opt?.value as UnitTypeLongPlural
|
||||
opt?.value as UnitTypeLongPlural,
|
||||
{ shouldDirty: true }
|
||||
);
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
|
||||
@@ -37,7 +37,7 @@ const UserProfile = () => {
|
||||
if (context.avatar) {
|
||||
showToast(t("your_user_profile_updated_successfully"), "success");
|
||||
await utils.viewer.me.refetch();
|
||||
} else {
|
||||
} else
|
||||
try {
|
||||
if (eventTypes?.length === 0) {
|
||||
await Promise.all(
|
||||
@@ -50,9 +50,11 @@ const UserProfile = () => {
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
await utils.viewer.me.refetch();
|
||||
router.push("/");
|
||||
}
|
||||
await utils.viewer.me.refetch();
|
||||
const redirectUrl = localStorage.getItem("onBoardingRedirect");
|
||||
localStorage.removeItem("onBoardingRedirect");
|
||||
|
||||
redirectUrl ? router.push(redirectUrl) : router.push("/");
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("problem_saving_user_profile"), "error");
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import type { FormValues } from "@pages/settings/my-account/profile";
|
||||
import { useState } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import {
|
||||
Badge,
|
||||
TextField,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
Button,
|
||||
InputError,
|
||||
} from "@calcom/ui";
|
||||
import { MoreHorizontal, Flag, Trash, Send } from "@calcom/ui/components/icon";
|
||||
|
||||
type CustomEmailTextFieldProps = {
|
||||
formMethods: UseFormReturn<FormValues>;
|
||||
formMethodFieldName: keyof FormValues;
|
||||
errorMessage: string;
|
||||
emailVerified: boolean;
|
||||
emailPrimary: boolean;
|
||||
dataTestId: string;
|
||||
handleChangePrimary: () => void;
|
||||
handleVerifyEmail: () => void;
|
||||
handleItemDelete: () => void;
|
||||
};
|
||||
|
||||
const CustomEmailTextField = ({
|
||||
formMethods,
|
||||
formMethodFieldName,
|
||||
errorMessage,
|
||||
emailVerified,
|
||||
emailPrimary,
|
||||
dataTestId,
|
||||
handleChangePrimary,
|
||||
handleVerifyEmail,
|
||||
handleItemDelete,
|
||||
}: CustomEmailTextFieldProps) => {
|
||||
const { t } = useLocale();
|
||||
const [inputFocus, setInputFocus] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className={`border-default mt-2 flex items-center rounded-md border ${
|
||||
inputFocus ? "ring-brand-default border-neutral-300 ring-2" : ""
|
||||
}`}>
|
||||
<TextField
|
||||
{...formMethods.register(formMethodFieldName)}
|
||||
label=""
|
||||
containerClassName="flex flex-1 items-center"
|
||||
className="mb-0 border-none outline-none focus:ring-0"
|
||||
data-testid={dataTestId}
|
||||
onFocus={() => setInputFocus(true)}
|
||||
onBlur={() => setInputFocus(false)}
|
||||
/>
|
||||
<div className="flex items-center pr-2">
|
||||
{emailPrimary && (
|
||||
<Badge variant="blue" size="sm" data-testid={`${dataTestId}-primary-badge`}>
|
||||
{t("primary")}
|
||||
</Badge>
|
||||
)}
|
||||
{!emailVerified && (
|
||||
<Badge variant="orange" size="sm" className="ml-2" data-testid={`${dataTestId}-unverified-badge`}>
|
||||
{t("unverified")}
|
||||
</Badge>
|
||||
)}
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
StartIcon={MoreHorizontal}
|
||||
variant="icon"
|
||||
size="sm"
|
||||
color="secondary"
|
||||
className="ml-2 rounded-md"
|
||||
data-testid="secondary-email-action-group-button"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
StartIcon={Flag}
|
||||
color="secondary"
|
||||
className="disabled:opacity-40"
|
||||
onClick={handleChangePrimary}
|
||||
disabled={!emailVerified || emailPrimary}
|
||||
data-testid="secondary-email-make-primary-button">
|
||||
{t("make_primary")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
{!emailVerified && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
StartIcon={Send}
|
||||
color="secondary"
|
||||
className="disabled:opacity-40"
|
||||
onClick={handleVerifyEmail}
|
||||
disabled={emailVerified}
|
||||
data-testid="resend-verify-email-button">
|
||||
{t("resend_email")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
StartIcon={Trash}
|
||||
color="destructive"
|
||||
className="disabled:opacity-40"
|
||||
onClick={handleItemDelete}
|
||||
disabled={emailPrimary}
|
||||
data-testid="secondary-email-delete-button">
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{errorMessage && <InputError message={errorMessage} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default CustomEmailTextField;
|
||||
@@ -0,0 +1,31 @@
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Dialog, DialogClose, DialogContent, DialogFooter } from "@calcom/ui";
|
||||
|
||||
interface SecondaryEmailConfirmModalProps {
|
||||
email: string;
|
||||
onCancel: () => void;
|
||||
}
|
||||
|
||||
const SecondaryEmailConfirmModal = ({ email, onCancel }: SecondaryEmailConfirmModalProps) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Dialog open={true}>
|
||||
<DialogContent
|
||||
title={t("confirm_email")}
|
||||
description={<Trans i18nKey="confirm_email_description" values={{ email }} />}
|
||||
type="creation"
|
||||
data-testid="secondary-email-confirm-dialog">
|
||||
<DialogFooter>
|
||||
<DialogClose color="primary" onClick={onCancel} data-testid="secondary-email-confirm-done-button">
|
||||
{t("done")}
|
||||
</DialogClose>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondaryEmailConfirmModal;
|
||||
@@ -0,0 +1,77 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogClose,
|
||||
Button,
|
||||
TextField,
|
||||
Form,
|
||||
InputError,
|
||||
} from "@calcom/ui";
|
||||
|
||||
interface SecondaryEmailModalProps {
|
||||
isLoading: boolean;
|
||||
errorMessage?: string;
|
||||
handleAddEmail: (value: { email: string }) => void;
|
||||
onCancel: () => void;
|
||||
clearErrorMessage: () => void;
|
||||
}
|
||||
|
||||
const SecondaryEmailModal = ({
|
||||
isLoading,
|
||||
errorMessage,
|
||||
handleAddEmail,
|
||||
onCancel,
|
||||
clearErrorMessage,
|
||||
}: SecondaryEmailModalProps) => {
|
||||
const { t } = useLocale();
|
||||
type FormValues = {
|
||||
email: string;
|
||||
};
|
||||
const formMethods = useForm<FormValues>({
|
||||
resolver: zodResolver(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
// We will reset the errorMessage once the user starts modifying the email
|
||||
const subscription = formMethods.watch(() => clearErrorMessage());
|
||||
return () => subscription.unsubscribe();
|
||||
}, [formMethods.watch]);
|
||||
|
||||
return (
|
||||
<Dialog open={true}>
|
||||
<DialogContent
|
||||
title={t("add_email")}
|
||||
description={t("add_email_description")}
|
||||
type="creation"
|
||||
data-testid="secondary-email-add-dialog">
|
||||
<Form form={formMethods} handleSubmit={handleAddEmail}>
|
||||
<TextField
|
||||
label={t("email_address")}
|
||||
data-testid="secondary-email-input"
|
||||
{...formMethods.register("email")}
|
||||
/>
|
||||
{errorMessage && <InputError message={errorMessage} />}
|
||||
<DialogFooter showDivider className="mt-10">
|
||||
<DialogClose onClick={onCancel}>{t("cancel")}</DialogClose>
|
||||
<Button type="submit" data-testid="add-secondary-email-button" disabled={isLoading}>
|
||||
{t("add_email")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default SecondaryEmailModal;
|
||||
@@ -160,7 +160,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
<div className="bg-subtle flex w-full flex-wrap gap-6 rounded-sm px-2 py-3 text-sm">
|
||||
<div>
|
||||
<p className="text-subtle">{t("current_username")}</p>
|
||||
<p className="text-emphasis mt-1" data-testid="current-username">
|
||||
<p className="text-emphasis mt-1 break-all" data-testid="current-username">
|
||||
{currentUsername}
|
||||
</p>
|
||||
</div>
|
||||
@@ -168,7 +168,10 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
||||
<p className="text-subtle" data-testid="new-username">
|
||||
{t("new_username")}
|
||||
</p>
|
||||
<p className="text-emphasis mt-1">{inputUsernameValue}</p>
|
||||
|
||||
<p className="text-emphasis min-h-5 mt-1 max-h-12 overflow-y-auto break-all">
|
||||
{inputUsernameValue}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -113,7 +113,7 @@ const enum ThemeSupport {
|
||||
// e.g. Login Page
|
||||
None = "none",
|
||||
// Entire App except Booking Pages
|
||||
App = "systemOnly",
|
||||
App = "userConfigured",
|
||||
// Booking Pages(including Routing Forms)
|
||||
Booking = "userConfigured",
|
||||
}
|
||||
|
||||
@@ -127,9 +127,9 @@ const enum ThemeSupport {
|
||||
// e.g. Login Page
|
||||
None = "none",
|
||||
// Entire App except Booking Pages
|
||||
App = "systemOnly",
|
||||
App = "appConfigured",
|
||||
// Booking Pages(including Routing Forms)
|
||||
Booking = "userConfigured",
|
||||
Booking = "bookingConfigured",
|
||||
}
|
||||
|
||||
type CalcomThemeProps = PropsWithChildren<
|
||||
@@ -144,8 +144,10 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => {
|
||||
const embedNamespace = getEmbedNamespace(props.router.query);
|
||||
const isEmbedMode = typeof embedNamespace === "string";
|
||||
|
||||
const themeProviderProps = getThemeProviderProps({ props, isEmbedMode, embedNamespace });
|
||||
|
||||
return (
|
||||
<ThemeProvider {...getThemeProviderProps({ props, isEmbedMode, embedNamespace })}>
|
||||
<ThemeProvider {...themeProviderProps}>
|
||||
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||
{typeof window !== "undefined" && !isEmbedMode && (
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
|
||||
// If feature flag is disabled, return not found on getServerSideProps
|
||||
export const getServerSideProps = async () => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
const insightsEnabled = await getFeatureFlag(prisma, "insights");
|
||||
|
||||
if (flags.insights === false) {
|
||||
if (!insightsEnabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
|
||||
@@ -696,7 +696,7 @@ describe("orgMigration", () => {
|
||||
});
|
||||
|
||||
describe("when user email matches orgAutoAcceptEmail", () => {
|
||||
const orgMetadata = {
|
||||
const orgSettings = {
|
||||
orgAutoAcceptEmail: "org1.com",
|
||||
} as const;
|
||||
|
||||
@@ -725,8 +725,10 @@ describe("orgMigration", () => {
|
||||
const dbOrg = await createOrg({
|
||||
slug: data.targetOrg.slug,
|
||||
name: data.targetOrg.name,
|
||||
metadata: {
|
||||
...orgMetadata,
|
||||
organizationSettings: {
|
||||
create: {
|
||||
...orgSettings,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -784,8 +786,10 @@ describe("orgMigration", () => {
|
||||
const dbOrg = await createOrg({
|
||||
slug: data.targetOrg.slug,
|
||||
name: data.targetOrg.name,
|
||||
metadata: {
|
||||
...orgMetadata,
|
||||
organizationSettings: {
|
||||
create: {
|
||||
...orgSettings,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -837,7 +841,11 @@ describe("orgMigration", () => {
|
||||
name: data.targetOrg.name,
|
||||
metadata: {
|
||||
requestedSlug: data.targetOrg.requestedSlug,
|
||||
...orgMetadata,
|
||||
},
|
||||
organizationSettings: {
|
||||
create: {
|
||||
...orgSettings,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -901,9 +909,7 @@ describe("orgMigration", () => {
|
||||
id: data.targetOrg.id,
|
||||
slug: data.targetOrg.slug,
|
||||
name: data.targetOrg.name,
|
||||
metadata: {
|
||||
isOrganization: true,
|
||||
},
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1564,9 +1570,9 @@ async function createOrg(
|
||||
return await prismock.team.create({
|
||||
data: {
|
||||
...data,
|
||||
isOrganization: true,
|
||||
metadata: {
|
||||
...(data.metadata || {}),
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ export async function moveUserToOrg({
|
||||
|
||||
const teamMetadata = teamMetadataSchema.parse(team?.metadata);
|
||||
|
||||
if (!teamMetadata?.isOrganization) {
|
||||
if (!team.isOrganization) {
|
||||
throw new Error(`Team with ID:${targetOrgId} is not an Org`);
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ export async function moveUserToOrg({
|
||||
if (!targetOrgUsername) {
|
||||
targetOrgUsername = getOrgUsernameFromEmail(
|
||||
userToMoveToOrg.email,
|
||||
targetOrganization.metadata.orgAutoAcceptEmail || ""
|
||||
targetOrganization.organizationSettings?.orgAutoAcceptEmail || ""
|
||||
);
|
||||
}
|
||||
|
||||
@@ -196,7 +196,7 @@ export async function moveTeamToOrg({
|
||||
|
||||
const teamMetadata = teamMetadataSchema.parse(possibleOrg?.metadata);
|
||||
|
||||
if (!teamMetadata?.isOrganization) {
|
||||
if (!possibleOrg.isOrganization) {
|
||||
throw new Error(`${targetOrg.id} is not an Org`);
|
||||
}
|
||||
|
||||
@@ -205,7 +205,7 @@ export async function moveTeamToOrg({
|
||||
await addTeamRedirect({
|
||||
oldTeamSlug,
|
||||
teamSlug: updatedTeam.slug,
|
||||
orgSlug: targetOrganization.slug || orgMetadata.requestedSlug || null,
|
||||
orgSlug: targetOrganization.slug || orgMetadata?.requestedSlug || null,
|
||||
});
|
||||
await setOrgSlugIfNotSet({ slug: targetOrganization.slug }, orgMetadata, targetOrg.id);
|
||||
if (moveMembers) {
|
||||
@@ -329,13 +329,14 @@ async function setOrgSlugIfNotSet(
|
||||
},
|
||||
orgMetadata: {
|
||||
requestedSlug?: string | null | undefined;
|
||||
},
|
||||
} | null,
|
||||
targetOrgId: number
|
||||
) {
|
||||
if (targetOrganization.slug) {
|
||||
return;
|
||||
}
|
||||
if (!orgMetadata.requestedSlug) {
|
||||
|
||||
if (!orgMetadata?.requestedSlug) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: `Org with id: ${targetOrgId} doesn't have a slug. Tried using requestedSlug but that's also not present. So, all migration done but failed to set the Organization slug. Please set it manually`,
|
||||
@@ -372,6 +373,9 @@ async function getTeamOrThrowError(targetOrgId: number) {
|
||||
where: {
|
||||
id: targetOrgId,
|
||||
},
|
||||
include: {
|
||||
organizationSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
@@ -650,6 +654,7 @@ async function moveTeamsWithoutMembersToOrg({
|
||||
id: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -661,7 +666,7 @@ async function moveTeamsWithoutMembersToOrg({
|
||||
};
|
||||
})
|
||||
// Remove Orgs from the list
|
||||
.filter((team) => !team.metadata?.isOrganization);
|
||||
.filter((team) => !team.isOrganization);
|
||||
|
||||
const teamIdsToBeMovedToOrg = teamsToBeMovedToOrg.map((t) => t.id);
|
||||
|
||||
@@ -814,6 +819,7 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use
|
||||
id: true,
|
||||
slug: true,
|
||||
metadata: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -825,7 +831,7 @@ async function removeTeamsWithoutItsMemberFromOrg({ userToRemoveFromOrg }: { use
|
||||
};
|
||||
})
|
||||
// Remove Orgs from the list
|
||||
.filter((team) => !team.metadata?.isOrganization);
|
||||
.filter((team) => !team.isOrganization);
|
||||
|
||||
const teamIdsToBeRemovedFromOrg = teamsToBeRemovedFromOrg.map((t) => t.id);
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
const organizations = await getFeatureFlag(prisma, "organizations");
|
||||
// Check if organizations are enabled
|
||||
if (flags["organizations"] !== true) {
|
||||
if (!organizations) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
|
||||
@@ -4,7 +4,7 @@ import { z } from "zod";
|
||||
import { getOrgUsernameFromEmail } from "@calcom/features/auth/signup/utils/getOrgUsernameFromEmail";
|
||||
import { checkPremiumUsername } from "@calcom/features/ee/common/lib/checkPremiumUsername";
|
||||
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -24,11 +24,22 @@ const querySchema = z.object({
|
||||
|
||||
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
const signupDisabled = await getFeatureFlag(prisma, "disable-signup");
|
||||
const ssr = await ssrInit(ctx);
|
||||
const token = z.string().optional().parse(ctx.query.token);
|
||||
const redirectUrlData = z
|
||||
.string()
|
||||
.refine((value) => value.startsWith(WEBAPP_URL), {
|
||||
params: (value: string) => ({ value }),
|
||||
message: "Redirect URL must start with 'cal.com'",
|
||||
})
|
||||
.optional()
|
||||
.safeParse(ctx.query.redirect);
|
||||
|
||||
const redirectUrl = redirectUrlData.success && redirectUrlData.data ? redirectUrlData.data : null;
|
||||
|
||||
const props = {
|
||||
redirectUrl,
|
||||
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
||||
isSAMLLoginEnabled,
|
||||
trpcState: ssr.dehydrate(),
|
||||
@@ -38,7 +49,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
// username + email prepopulated from query params
|
||||
const { username: preFillusername, email: prefilEmail } = querySchema.parse(ctx.query);
|
||||
|
||||
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || flags["disable-signup"]) {
|
||||
if ((process.env.NEXT_PUBLIC_DISABLE_SIGNUP === "true" && !token) || signupDisabled) {
|
||||
return {
|
||||
notFound: true,
|
||||
} as const;
|
||||
@@ -67,14 +78,17 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
team: {
|
||||
select: {
|
||||
metadata: true,
|
||||
isOrganization: true,
|
||||
parentId: true,
|
||||
parent: {
|
||||
select: {
|
||||
slug: true,
|
||||
metadata: true,
|
||||
isOrganization: true,
|
||||
organizationSettings: true,
|
||||
},
|
||||
},
|
||||
slug: true,
|
||||
organizationSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -123,8 +137,8 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
};
|
||||
|
||||
const isATeamInOrganization = tokenTeam?.parentId !== null;
|
||||
const isOrganization = tokenTeam.metadata?.isOrganization;
|
||||
// Detect if the team is an org by either the metadata flag or if it has a parent team
|
||||
const isOrganization = tokenTeam.isOrganization;
|
||||
const isOrganizationOrATeamInOrganization = isOrganization || isATeamInOrganization;
|
||||
// If we are dealing with an org, the slug may come from the team itself or its parent
|
||||
const orgSlug = isOrganizationOrATeamInOrganization
|
||||
@@ -141,9 +155,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
|
||||
const isValidEmail = checkValidEmail(verificationToken.identifier);
|
||||
const isOrgInviteByLink = isOrganizationOrATeamInOrganization && !isValidEmail;
|
||||
const parentMetaDataForSubteam = tokenTeam?.parent?.metadata
|
||||
? teamMetadataSchema.parse(tokenTeam.parent.metadata)
|
||||
: null;
|
||||
const parentOrgSettings = tokenTeam?.parent?.organizationSettings ?? null;
|
||||
|
||||
return {
|
||||
props: {
|
||||
@@ -156,15 +168,15 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||
? getOrgUsernameFromEmail(
|
||||
verificationToken.identifier,
|
||||
(isOrganization
|
||||
? tokenTeam.metadata?.orgAutoAcceptEmail
|
||||
: parentMetaDataForSubteam?.orgAutoAcceptEmail) || ""
|
||||
? tokenTeam.organizationSettings?.orgAutoAcceptEmail
|
||||
: parentOrgSettings?.orgAutoAcceptEmail) || ""
|
||||
)
|
||||
: slugify(username),
|
||||
}
|
||||
: null,
|
||||
orgSlug,
|
||||
orgAutoAcceptEmail: isOrgInviteByLink
|
||||
? tokenTeam?.metadata?.orgAutoAcceptEmail ?? parentMetaDataForSubteam?.orgAutoAcceptEmail ?? null
|
||||
? tokenTeam?.organizationSettings?.orgAutoAcceptEmail ?? parentOrgSettings?.orgAutoAcceptEmail ?? null
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { getFeatureFlag } from "@calcom/features/flags/server/utils";
|
||||
import { getBookerBaseUrlSync } from "@calcom/lib/getBookerUrl/client";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
@@ -37,12 +37,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
// Provided by Rewrite from next.config.js
|
||||
const isOrgProfile = context.query?.isOrgProfile === "1";
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
const isOrganizationFeatureEnabled = flags["organizations"];
|
||||
const organizationsEnabled = await getFeatureFlag(prisma, "organizations");
|
||||
|
||||
log.debug("getServerSideProps", {
|
||||
isOrgProfile,
|
||||
isOrganizationFeatureEnabled,
|
||||
isOrganizationFeatureEnabled: organizationsEnabled,
|
||||
isValidOrgDomain,
|
||||
currentOrgDomain,
|
||||
});
|
||||
@@ -73,8 +72,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
// Taking care of sub-teams and orgs
|
||||
if (
|
||||
(!isValidOrgDomain && team?.parent) ||
|
||||
(!isValidOrgDomain && !!metadata?.isOrganization) ||
|
||||
!isOrganizationFeatureEnabled
|
||||
(!isValidOrgDomain && !!team?.isOrganization) ||
|
||||
!organizationsEnabled
|
||||
) {
|
||||
return { notFound: true } as const;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import { getEventName } from "@calcom/core/event";
|
||||
import type { ConfigType } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
sdkActionManager,
|
||||
useEmbedNonStylesConfig,
|
||||
useIsBackgroundTransparent,
|
||||
useIsEmbed,
|
||||
@@ -193,22 +192,6 @@ export default function Success(props: PageProps) {
|
||||
}, [telemetry]); */
|
||||
|
||||
useEffect(() => {
|
||||
const users = eventType.users;
|
||||
if (!sdkActionManager) return;
|
||||
// TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add
|
||||
sdkActionManager.fire("bookingSuccessful", {
|
||||
booking: bookingInfo,
|
||||
eventType,
|
||||
date: date.toString(),
|
||||
duration: calculatedDuration,
|
||||
organizer: {
|
||||
name: users[0].name || "Nameless",
|
||||
email: bookingInfo?.userPrimaryEmail || users[0].email || "Email-less",
|
||||
timeZone: users[0].timeZone,
|
||||
},
|
||||
confirmed: !needsConfirmation,
|
||||
// TODO: Add payment details
|
||||
});
|
||||
setDate(
|
||||
date.tz(localStorage.getItem("timeOption.preferredTimeZone") || dayjs.tz.guess() || "Europe/London")
|
||||
);
|
||||
@@ -294,6 +277,16 @@ export default function Success(props: PageProps) {
|
||||
const providerName = guessEventLocationType(location)?.label;
|
||||
const rescheduleProviderName = guessEventLocationType(rescheduleLocation)?.label;
|
||||
|
||||
const bookingCancelledEventProps = {
|
||||
booking: bookingInfo,
|
||||
organizer: {
|
||||
name: bookingInfo?.user?.name || "Nameless",
|
||||
email: bookingInfo?.userPrimaryEmail || bookingInfo?.user?.email || "Email-less",
|
||||
timeZone: bookingInfo?.user?.timeZone,
|
||||
},
|
||||
eventType,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
|
||||
{!isEmbed && (
|
||||
@@ -610,6 +603,7 @@ export default function Success(props: PageProps) {
|
||||
theme={isSuccessBookingPage ? props.profile.theme : "light"}
|
||||
allRemainingBookings={allRemainingBookings}
|
||||
seatReferenceUid={seatReferenceUid}
|
||||
bookingCancelledEventProps={bookingCancelledEventProps}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
|
||||
@@ -425,7 +425,7 @@ export const EventTypeList = ({
|
||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.team && !isManagedEventType && (
|
||||
{!!type.teamId && !isManagedEventType && (
|
||||
<UserAvatarGroup
|
||||
className="relative right-3"
|
||||
size="sm"
|
||||
|
||||
@@ -119,7 +119,7 @@ export type FormValues = {
|
||||
schedule: number | null;
|
||||
periodType: PeriodType;
|
||||
periodDays: number;
|
||||
periodCountCalendarDays: "1" | "0";
|
||||
periodCountCalendarDays: boolean;
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
@@ -149,6 +149,7 @@ export type FormValues = {
|
||||
users: EventTypeSetup["users"];
|
||||
assignAllTeamMembers: boolean;
|
||||
useEventTypeDestinationCalendarEmail: boolean;
|
||||
secondaryEmailId?: number;
|
||||
};
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
@@ -192,14 +193,17 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
formMethods.setValue(
|
||||
"children",
|
||||
formMethods.getValues().children.map((child) => ({
|
||||
...child,
|
||||
created: true,
|
||||
}))
|
||||
);
|
||||
formMethods.setValue("assignAllTeamMembers", formMethods.getValues("assignAllTeamMembers") || false);
|
||||
const currentValues = formMethods.getValues();
|
||||
|
||||
currentValues.children = currentValues.children.map((child) => ({
|
||||
...child,
|
||||
created: true,
|
||||
}));
|
||||
currentValues.assignAllTeamMembers = currentValues.assignAllTeamMembers || false;
|
||||
|
||||
// Reset the form with these values as new default values to ensure the correct comparison for dirtyFields eval
|
||||
formMethods.reset(currentValues);
|
||||
|
||||
showToast(t("event_type_updated_successfully", { eventTypeTitle: eventType.title }), "success");
|
||||
},
|
||||
async onSettled() {
|
||||
@@ -253,7 +257,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
eventType.bookingFields.forEach(({ name }) => {
|
||||
bookingFields[name] = name;
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const defaultValues: any = useMemo(() => {
|
||||
return {
|
||||
@@ -264,7 +267,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
beforeEventBuffer: eventType.beforeEventBuffer,
|
||||
eventName: eventType.eventName || "",
|
||||
scheduleName: eventType.scheduleName,
|
||||
periodDays: eventType.periodDays || 30,
|
||||
periodDays: eventType.periodDays,
|
||||
requiresBookerEmailVerification: eventType.requiresBookerEmailVerification,
|
||||
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
|
||||
seatsShowAttendees: eventType.seatsShowAttendees,
|
||||
@@ -290,7 +293,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
offsetStart: eventType.offsetStart,
|
||||
bookingFields: eventType.bookingFields,
|
||||
periodType: eventType.periodType,
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? true : false,
|
||||
schedulingType: eventType.schedulingType,
|
||||
requiresConfirmation: eventType.requiresConfirmation,
|
||||
slotInterval: eventType.slotInterval,
|
||||
@@ -300,6 +303,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
successRedirectUrl: eventType.successRedirectUrl || "",
|
||||
users: eventType.users,
|
||||
useEventTypeDestinationCalendarEmail: eventType.useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId: eventType?.secondaryEmailId || -1,
|
||||
children: eventType.children.map((ch) => ({
|
||||
...ch,
|
||||
created: true,
|
||||
@@ -316,7 +320,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
assignAllTeamMembers: eventType.assignAllTeamMembers,
|
||||
};
|
||||
}, [eventType, periodDates, metadata]);
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(
|
||||
@@ -405,14 +408,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
.passthrough()
|
||||
),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!formMethods.formState.isDirty) {
|
||||
//TODO: What's the best way to sync the form with backend
|
||||
formMethods.setValue("bookingFields", defaultValues.bookingFields);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [defaultValues]);
|
||||
const {
|
||||
formState: { isDirty: isFormDirty, dirtyFields },
|
||||
} = formMethods;
|
||||
|
||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||
const availability = formMethods.watch("availability");
|
||||
@@ -453,8 +451,100 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
),
|
||||
webhooks: <EventWebhooksTab eventType={eventType} />,
|
||||
} as const;
|
||||
const isObject = <T,>(value: T): boolean => {
|
||||
return value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
};
|
||||
|
||||
const isArray = <T,>(value: T): boolean => {
|
||||
return Array.isArray(value);
|
||||
};
|
||||
|
||||
const isFieldDirty = (fieldName: keyof FormValues) => {
|
||||
// If the field itself is directly marked as dirty
|
||||
if (dirtyFields[fieldName] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if the field is an object or an array
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const fieldValue: any = getNestedField(dirtyFields, fieldName);
|
||||
if (isObject(fieldValue)) {
|
||||
for (const key in fieldValue) {
|
||||
if (fieldValue[key] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(fieldValue[key]) || isArray(fieldValue[key])) {
|
||||
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
|
||||
// Recursive call for nested objects or arrays
|
||||
if (isFieldDirty(nestedFieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isArray(fieldValue)) {
|
||||
for (const element of fieldValue) {
|
||||
// If element is an object, check each property of the object
|
||||
if (isObject(element)) {
|
||||
for (const key in element) {
|
||||
if (element[key] === true) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isObject(element[key]) || isArray(element[key])) {
|
||||
const nestedFieldName = `${fieldName}.${key}` as keyof FormValues;
|
||||
// Recursive call for nested objects or arrays within each element
|
||||
if (isFieldDirty(nestedFieldName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (element === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const getNestedField = (obj: typeof dirtyFields, path: string) => {
|
||||
const keys = path.split(".");
|
||||
let current = obj;
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
// @ts-expect-error /—— currentKey could be any deeply nested fields thanks to recursion
|
||||
const currentKey = current[keys[i]];
|
||||
if (currentKey === undefined) return undefined;
|
||||
current = currentKey;
|
||||
}
|
||||
|
||||
return current;
|
||||
};
|
||||
|
||||
const getDirtyFields = (values: FormValues): Partial<FormValues> => {
|
||||
if (!isFormDirty) {
|
||||
return {};
|
||||
}
|
||||
const updatedFields: Partial<FormValues> = {};
|
||||
Object.keys(dirtyFields).forEach((key) => {
|
||||
const typedKey = key as keyof typeof dirtyFields;
|
||||
updatedFields[typedKey] = undefined;
|
||||
const isDirty = isFieldDirty(typedKey);
|
||||
if (isDirty) {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
updatedFields[typedKey] = values[typedKey];
|
||||
}
|
||||
});
|
||||
return updatedFields;
|
||||
};
|
||||
|
||||
const handleSubmit = async (values: FormValues) => {
|
||||
const { children } = values;
|
||||
const dirtyValues = getDirtyFields(values);
|
||||
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
@@ -470,7 +560,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
locations,
|
||||
metadata,
|
||||
customInputs,
|
||||
children,
|
||||
assignAllTeamMembers,
|
||||
// We don't need to send send these values to the backend
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
@@ -483,8 +572,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
multipleDurationEnabled,
|
||||
length,
|
||||
...input
|
||||
} = values;
|
||||
|
||||
} = dirtyValues;
|
||||
if (!Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
|
||||
if (bookingLimits) {
|
||||
@@ -504,8 +592,12 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
if (metadata?.multipleDuration.length < 1) {
|
||||
throw new Error(t("event_setup_multiple_duration_error"));
|
||||
} else {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
// if length is unchanged, we skip this check
|
||||
if (length !== undefined) {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -521,14 +613,14 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
const payload = {
|
||||
...rest,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
@@ -542,7 +634,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
customInputs,
|
||||
children,
|
||||
assignAllTeamMembers,
|
||||
});
|
||||
};
|
||||
// Filter out undefined values
|
||||
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
}
|
||||
};
|
||||
|
||||
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
|
||||
@@ -595,6 +699,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
handleSubmit={async (values) => {
|
||||
const { children } = values;
|
||||
const dirtyValues = getDirtyFields(values);
|
||||
const dirtyFieldExists = Object.keys(dirtyValues).length !== 0;
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
@@ -617,9 +724,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
multipleDurationEnabled,
|
||||
length,
|
||||
...input
|
||||
} = values;
|
||||
} = dirtyValues;
|
||||
|
||||
if (!Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
if (length && !Number(length)) throw new Error(t("event_setup_length_error"));
|
||||
|
||||
if (bookingLimits) {
|
||||
const isValid = validateIntervalLimitOrder(bookingLimits);
|
||||
@@ -638,8 +745,11 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
if (metadata?.multipleDuration.length < 1) {
|
||||
throw new Error(t("event_setup_multiple_duration_error"));
|
||||
} else {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
if (length !== undefined) {
|
||||
if (!length && !metadata?.multipleDuration?.includes(length)) {
|
||||
//This would work but it leaves the potential of this check being useless. Need to check against length and not eventType.length, but length can be undefined
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -651,14 +761,15 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { availability, users, scheduleName, ...rest } = input;
|
||||
updateMutation.mutate({
|
||||
const payload = {
|
||||
...rest,
|
||||
children,
|
||||
length,
|
||||
locations,
|
||||
recurringEvent,
|
||||
periodStartDate: periodDates.startDate,
|
||||
periodEndDate: periodDates.endDate,
|
||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||
periodStartDate: periodDates?.startDate,
|
||||
periodEndDate: periodDates?.endDate,
|
||||
periodCountCalendarDays,
|
||||
id: eventType.id,
|
||||
beforeEventBuffer,
|
||||
afterEventBuffer,
|
||||
@@ -670,7 +781,19 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
seatsShowAvailabilityCount,
|
||||
metadata,
|
||||
customInputs,
|
||||
});
|
||||
};
|
||||
// Filter out undefined values
|
||||
const filteredPayload = Object.entries(payload).reduce((acc, [key, value]) => {
|
||||
if (value !== undefined) {
|
||||
// @ts-expect-error Element implicitly has any type
|
||||
acc[key] = value;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (dirtyFieldExists) {
|
||||
updateMutation.mutate({ ...filteredPayload, id: eventType.id });
|
||||
}
|
||||
}}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
</Form>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { handleUserRedirection } from "@calcom/features/booking-redirect/handle-
|
||||
import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
|
||||
import { DEFAULT_DARK_BRAND_COLOR, DEFAULT_LIGHT_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import { getUsernameList } from "@calcom/lib/defaultEvents";
|
||||
import { getUserAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
@@ -206,7 +207,10 @@ export const getServerSideProps: GetServerSideProps<UserPageProps> = async (cont
|
||||
|
||||
const profile = {
|
||||
name: user.name || user.username || "",
|
||||
image: user.avatar,
|
||||
image: getUserAvatarUrl({
|
||||
...user,
|
||||
profile: user.profile,
|
||||
}),
|
||||
theme: user.theme,
|
||||
brandColor: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR,
|
||||
avatarUrl: user.avatarUrl,
|
||||
|
||||
@@ -155,7 +155,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
pathname: `/${user.profile.username}/${type.slug}`,
|
||||
query,
|
||||
}}
|
||||
passHref
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.7.18",
|
||||
"version": "3.8.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
const oldPassword = req.body.oldPassword;
|
||||
const newPassword = req.body.newPassword;
|
||||
|
||||
const currentPassword = user.password;
|
||||
const currentPassword = user.password?.hash;
|
||||
if (!currentPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
@@ -53,12 +53,16 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
}
|
||||
|
||||
const hashedPassword = await hashPassword(newPassword);
|
||||
await prisma.user.update({
|
||||
await prisma.userPassword.upsert({
|
||||
where: {
|
||||
id: user.id,
|
||||
userId: user.id,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
create: {
|
||||
hash: hashedPassword,
|
||||
userId: user.id,
|
||||
},
|
||||
update: {
|
||||
hash: hashedPassword,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
email: maybeRequest.email,
|
||||
},
|
||||
data: {
|
||||
password: hashedPassword,
|
||||
password: {
|
||||
upsert: {
|
||||
create: { hash: hashedPassword },
|
||||
update: { hash: hashedPassword },
|
||||
},
|
||||
},
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ async function handler(req: NextApiRequest) {
|
||||
data: {
|
||||
username,
|
||||
email: userEmail,
|
||||
password: hashedPassword,
|
||||
password: { create: { hash: hashedPassword } },
|
||||
role: "ADMIN",
|
||||
name: parsedQuery.data.full_name,
|
||||
emailVerified: new Date(),
|
||||
|
||||
@@ -23,13 +23,13 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
}
|
||||
|
||||
if (!user.password && user.identityProvider === IdentityProvider.CAL) {
|
||||
if (!user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
|
||||
@@ -37,8 +37,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.json({ message: "Two factor disabled" });
|
||||
}
|
||||
|
||||
if (user.password && user.identityProvider === IdentityProvider.CAL) {
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
|
||||
if (user.password?.hash && user.identityProvider === IdentityProvider.CAL) {
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id } });
|
||||
const user = await prisma.user.findUnique({ where: { id: session.user.id }, include: { password: true } });
|
||||
if (!user) {
|
||||
console.error(`Session references user that no longer exists.`);
|
||||
return res.status(401).json({ message: "Not authenticated" });
|
||||
@@ -35,7 +35,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(400).json({ error: ErrorCode.ThirdPartyIdentityProviderEnabled });
|
||||
}
|
||||
|
||||
if (!user.password) {
|
||||
if (!user.password?.hash) {
|
||||
return res.status(400).json({ error: ErrorCode.UserMissingPassword });
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(500).json({ error: ErrorCode.InternalServerError });
|
||||
}
|
||||
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password);
|
||||
const isCorrectPassword = await verifyPassword(req.body.password, user.password.hash);
|
||||
if (!isCorrectPassword) {
|
||||
return res.status(400).json({ error: ErrorCode.IncorrectPassword });
|
||||
}
|
||||
|
||||
@@ -10,6 +10,8 @@ const verifySchema = z.object({
|
||||
token: z.string(),
|
||||
});
|
||||
|
||||
const USER_ALREADY_EXISTING_MESSAGE = "A User already exists with this email";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { token } = verifySchema.parse(req.query);
|
||||
|
||||
@@ -27,6 +29,23 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(401).json({ message: "Token expired" });
|
||||
}
|
||||
|
||||
// The user is verifying the secondary email
|
||||
if (foundToken?.secondaryEmailId) {
|
||||
await prisma.secondaryEmail.update({
|
||||
where: {
|
||||
id: foundToken.secondaryEmailId,
|
||||
email: foundToken?.identifier,
|
||||
},
|
||||
data: {
|
||||
emailVerified: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
await cleanUpVerificationTokens(foundToken.id);
|
||||
|
||||
return res.redirect(`${WEBAPP_URL}/settings/my-account/profile`);
|
||||
}
|
||||
|
||||
const user = await prisma.user.findFirst({
|
||||
where: {
|
||||
email: foundToken?.identifier,
|
||||
@@ -49,9 +68,25 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
return res.status(401).json({ message: "A User already exists with this email" });
|
||||
return res.status(401).json({ message: USER_ALREADY_EXISTING_MESSAGE });
|
||||
}
|
||||
|
||||
// Ensure this email isn't being added by another user as secondary email
|
||||
const existingSecondaryUser = await prisma.secondaryEmail.findUnique({
|
||||
where: {
|
||||
email: userMetadataParsed?.emailChangeWaitingForVerification,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSecondaryUser && existingSecondaryUser.userId !== user.id) {
|
||||
return res.status(401).json({ message: USER_ALREADY_EXISTING_MESSAGE });
|
||||
}
|
||||
|
||||
const oldEmail = user.email;
|
||||
const updatedEmail = userMetadataParsed.emailChangeWaitingForVerification;
|
||||
delete userMetadataParsed.emailChangeWaitingForVerification;
|
||||
|
||||
@@ -66,6 +101,21 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
},
|
||||
});
|
||||
|
||||
// The user is trying to update the email to an already existing unverified secondary email of his
|
||||
// so we swap the emails and its verified status
|
||||
if (existingSecondaryUser?.userId === user.id) {
|
||||
await prisma.secondaryEmail.update({
|
||||
where: {
|
||||
id: existingSecondaryUser.id,
|
||||
userId: user.id,
|
||||
},
|
||||
data: {
|
||||
email: oldEmail,
|
||||
emailVerified: user.emailVerified,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await cleanUpVerificationTokens(foundToken.id);
|
||||
|
||||
return res.status(200).json({
|
||||
@@ -87,7 +137,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.redirect(`${WEBAPP_URL}/${hasCompletedOnboarding ? "/event-types" : "/getting-started"}`);
|
||||
}
|
||||
|
||||
async function cleanUpVerificationTokens(id: number) {
|
||||
export async function cleanUpVerificationTokens(id: number) {
|
||||
// Delete token from DB after it has been used
|
||||
await prisma.verificationToken.delete({
|
||||
where: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { CREDENTIAL_SYNC_SECRET, CREDENTIAL_SYNC_SECRET_HEADER_NAME } from "@calcom/lib/constants";
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -19,11 +20,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return res.status(403).json({ message: "Credential sharing is not enabled" });
|
||||
}
|
||||
|
||||
if (
|
||||
req.headers[process.env.CALCOM_WEBHOOK_HEADER_NAME || "calcom-webhook-secret"] !==
|
||||
process.env.CALCOM_WEBHOOK_SECRET
|
||||
) {
|
||||
return res.status(403).json({ message: "Invalid webhook secret" });
|
||||
if (req.headers[CREDENTIAL_SYNC_SECRET_HEADER_NAME] !== CREDENTIAL_SYNC_SECRET) {
|
||||
return res.status(403).json({ message: "Invalid credential sync secret" });
|
||||
}
|
||||
|
||||
const reqBody = appCredentialWebhookRequestBodySchema.parse(req.body);
|
||||
|
||||
@@ -47,7 +47,7 @@ export default function Apps({ apps }: PageProps) {
|
||||
<div className="mb-16">
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps
|
||||
.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||
?.sort((a, b) => (b.installCount || 0) - (a.installCount || 0))
|
||||
.map((app) => {
|
||||
return <AppCard key={app.slug} app={app} />;
|
||||
})}
|
||||
|
||||
@@ -95,6 +95,7 @@ const IntegrationsContainer = ({
|
||||
className="mb-6"
|
||||
actions={
|
||||
<Button
|
||||
data-testid="add-apps"
|
||||
href={variant ? `/apps/categories/${variant}` : "/apps"}
|
||||
color="secondary"
|
||||
StartIcon={Plus}>
|
||||
|
||||
@@ -11,8 +11,10 @@ import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants";
|
||||
import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours";
|
||||
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||
import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||
import type { userMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -29,6 +31,7 @@ import {
|
||||
SkeletonText,
|
||||
SettingsToggle,
|
||||
UpgradeTeamsBadge,
|
||||
useCalcomTheme,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
@@ -41,10 +44,10 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
|
||||
<SkeletonText className="h-8 w-1/3" />
|
||||
</div>
|
||||
<div className="border-subtle space-y-6 border-x px-4 py-6 sm:px-6">
|
||||
<div className="flex items-center justify-center">
|
||||
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
||||
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
||||
<SkeletonButton className="mr-6 h-32 w-48 rounded-md p-5" />
|
||||
<div className="[&>*]:bg-emphasis flex w-full items-center justify-center gap-x-2 [&>*]:animate-pulse">
|
||||
<div className="h-32 flex-1 rounded-md p-5" />
|
||||
<div className="h-32 flex-1 rounded-md p-5" />
|
||||
<div className="h-32 flex-1 rounded-md p-5" />
|
||||
</div>
|
||||
<div className="flex justify-between">
|
||||
<SkeletonText className="h-8 w-1/3" />
|
||||
@@ -62,6 +65,26 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
|
||||
);
|
||||
};
|
||||
|
||||
const useBrandColors = (
|
||||
currentTheme: string | null,
|
||||
{
|
||||
brandColor,
|
||||
darkBrandColor,
|
||||
}: {
|
||||
brandColor?: string | null;
|
||||
darkBrandColor?: string | null;
|
||||
}
|
||||
): void => {
|
||||
const brandTheme = useGetBrandingColours({
|
||||
lightVal: brandColor,
|
||||
darkVal: darkBrandColor,
|
||||
});
|
||||
const selectedTheme = currentTheme ? brandTheme[currentTheme as "light" | "dark"] : {};
|
||||
useCalcomTheme({
|
||||
root: selectedTheme,
|
||||
});
|
||||
};
|
||||
|
||||
const AppearanceView = ({
|
||||
user,
|
||||
hasPaidPlan,
|
||||
@@ -77,6 +100,22 @@ const AppearanceView = ({
|
||||
user?.brandColor !== DEFAULT_LIGHT_BRAND_COLOR || user?.darkBrandColor !== DEFAULT_DARK_BRAND_COLOR
|
||||
);
|
||||
const [hideBrandingValue, setHideBrandingValue] = useState(user?.hideBranding ?? false);
|
||||
useTheme(user?.appTheme);
|
||||
useBrandColors(user?.appTheme ?? null, {
|
||||
brandColor: user?.brandColor,
|
||||
darkBrandColor: user?.darkBrandColor,
|
||||
});
|
||||
|
||||
const userAppThemeFormMethods = useForm({
|
||||
defaultValues: {
|
||||
appTheme: user.appTheme,
|
||||
},
|
||||
});
|
||||
|
||||
const {
|
||||
formState: { isSubmitting: isUserAppThemeSubmitting, isDirty: isUserAppThemeDirty },
|
||||
reset: resetUserAppThemeReset,
|
||||
} = userAppThemeFormMethods;
|
||||
|
||||
const userThemeFormMethods = useForm({
|
||||
defaultValues: {
|
||||
@@ -131,6 +170,7 @@ const AppearanceView = ({
|
||||
resetBrandColorsThemeReset({ brandColor: data.brandColor, darkBrandColor: data.darkBrandColor });
|
||||
resetBookerLayoutThemeReset({ metadata: data.metadata });
|
||||
resetUserThemeReset({ theme: data.theme });
|
||||
resetUserAppThemeReset({ appTheme: data.appTheme });
|
||||
},
|
||||
onError: (error) => {
|
||||
if (error.message) {
|
||||
@@ -147,6 +187,56 @@ const AppearanceView = ({
|
||||
return (
|
||||
<div>
|
||||
<Meta title={t("appearance")} description={t("appearance_description")} borderInShellHeader={false} />
|
||||
<div className="border-subtle mt-6 flex items-center rounded-t-lg border p-6 text-sm">
|
||||
<div>
|
||||
<p className="text-default text-base font-semibold">{t("app_theme")}</p>
|
||||
<p className="text-default">{t("app_theme_applies_note")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<Form
|
||||
form={userAppThemeFormMethods}
|
||||
handleSubmit={(values) => {
|
||||
mutation.mutate({
|
||||
appTheme: values.appTheme ?? null,
|
||||
});
|
||||
}}>
|
||||
<div className="border-subtle flex flex-col justify-between border-x px-6 py-8 sm:flex-row">
|
||||
<ThemeLabel
|
||||
variant="system"
|
||||
value={undefined}
|
||||
label={t("theme_system")}
|
||||
defaultChecked={user.appTheme === null}
|
||||
register={userAppThemeFormMethods.register}
|
||||
fieldName="appTheme"
|
||||
/>
|
||||
<ThemeLabel
|
||||
variant="light"
|
||||
value="light"
|
||||
label={t("light")}
|
||||
defaultChecked={user.appTheme === "light"}
|
||||
register={userAppThemeFormMethods.register}
|
||||
fieldName="appTheme"
|
||||
/>
|
||||
<ThemeLabel
|
||||
variant="dark"
|
||||
value="dark"
|
||||
label={t("dark")}
|
||||
defaultChecked={user.appTheme === "dark"}
|
||||
register={userAppThemeFormMethods.register}
|
||||
fieldName="appTheme"
|
||||
/>
|
||||
</div>
|
||||
<SectionBottomActions className="mb-6" align="end">
|
||||
<Button
|
||||
disabled={isUserAppThemeSubmitting || !isUserAppThemeDirty}
|
||||
type="submit"
|
||||
data-testid="update-app-theme-btn"
|
||||
color="primary">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
</Form>
|
||||
|
||||
<div className="border-subtle mt-6 flex items-center rounded-t-lg border p-6 text-sm">
|
||||
<div>
|
||||
<p className="text-default text-base font-semibold">{t("theme")}</p>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { pick, get } from "lodash";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import type { BaseSyntheticEvent } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { Controller, useForm, useFieldArray } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
@@ -43,10 +45,13 @@ import {
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
import { UserAvatar } from "@calcom/ui";
|
||||
import { AlertTriangle, Trash2 } from "@calcom/ui/components/icon";
|
||||
import { AlertTriangle, Trash2, Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
import CustomEmailTextField from "@components/settings/CustomEmailTextField";
|
||||
import SecondaryEmailConfirmModal from "@components/settings/SecondaryEmailConfirmModal";
|
||||
import SecondaryEmailModal from "@components/settings/SecondaryEmailModal";
|
||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
|
||||
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
|
||||
@@ -72,12 +77,20 @@ interface DeleteAccountValues {
|
||||
totpCode: string;
|
||||
}
|
||||
|
||||
type FormValues = {
|
||||
type Email = {
|
||||
id: number;
|
||||
email: string;
|
||||
emailVerified: string | null;
|
||||
emailPrimary: boolean;
|
||||
};
|
||||
|
||||
export type FormValues = {
|
||||
username: string;
|
||||
avatar: string;
|
||||
name: string;
|
||||
email: string;
|
||||
bio: string;
|
||||
secondaryEmails: Email[];
|
||||
};
|
||||
|
||||
const ProfileView = () => {
|
||||
@@ -126,11 +139,27 @@ const ProfileView = () => {
|
||||
},
|
||||
});
|
||||
|
||||
const addSecondaryEmailMutation = trpc.viewer.addSecondaryEmail.useMutation({
|
||||
onSuccess: (res) => {
|
||||
setShowSecondaryEmailModalOpen(false);
|
||||
setNewlyAddedSecondaryEmail(res?.data?.email);
|
||||
utils.viewer.me.invalidate();
|
||||
},
|
||||
onError: (error) => {
|
||||
setSecondaryEmailAddErrorMessage(error?.message || "");
|
||||
},
|
||||
});
|
||||
|
||||
const resendVerifyEmailMutation = trpc.viewer.auth.resendVerifyEmail.useMutation();
|
||||
|
||||
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
|
||||
const [tempFormValues, setTempFormValues] = useState<FormValues | null>(null);
|
||||
const [tempFormValues, setTempFormValues] = useState<ExtendedFormValues | null>(null);
|
||||
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
|
||||
const [confirmAuthEmailChangeWarningDialogOpen, setConfirmAuthEmailChangeWarningDialogOpen] =
|
||||
useState(false);
|
||||
const [showSecondaryEmailModalOpen, setShowSecondaryEmailModalOpen] = useState(false);
|
||||
const [secondaryEmailAddErrorMessage, setSecondaryEmailAddErrorMessage] = useState("");
|
||||
const [newlyAddedSecondaryEmail, setNewlyAddedSecondaryEmail] = useState<undefined | string>(undefined);
|
||||
|
||||
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
||||
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
|
||||
@@ -232,6 +261,7 @@ const ProfileView = () => {
|
||||
);
|
||||
}
|
||||
|
||||
const userEmail = user.email || "";
|
||||
const defaultValues = {
|
||||
username: user.username || "",
|
||||
avatar: getUserAvatarUrl({
|
||||
@@ -239,8 +269,22 @@ const ProfileView = () => {
|
||||
profile: user.profile,
|
||||
}),
|
||||
name: user.name || "",
|
||||
email: user.email || "",
|
||||
email: userEmail,
|
||||
bio: user.bio || "",
|
||||
// We add the primary email as the first item in the list
|
||||
secondaryEmails: [
|
||||
{
|
||||
id: 0,
|
||||
email: userEmail,
|
||||
emailVerified: user.emailVerified?.toString() || null,
|
||||
emailPrimary: true,
|
||||
},
|
||||
...(user.secondaryEmails || []).map((secondaryEmail) => ({
|
||||
...secondaryEmail,
|
||||
emailVerified: secondaryEmail.emailVerified?.toString() || null,
|
||||
emailPrimary: false,
|
||||
})),
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -269,6 +313,11 @@ const ProfileView = () => {
|
||||
updateProfileMutation.mutate(values);
|
||||
}
|
||||
}}
|
||||
handleAddSecondaryEmail={() => setShowSecondaryEmailModalOpen(true)}
|
||||
handleResendVerifyEmail={(email) => {
|
||||
resendVerifyEmailMutation.mutate({ email });
|
||||
showToast(t("email_sent"), "success");
|
||||
}}
|
||||
extraField={
|
||||
<div className="mt-6">
|
||||
<UsernameAvailabilityField
|
||||
@@ -374,7 +423,7 @@ const ProfileView = () => {
|
||||
</div>
|
||||
<DialogFooter showDivider>
|
||||
<Button
|
||||
data-testId="profile-update-email-submit-button"
|
||||
data-testid="profile-update-email-submit-button"
|
||||
color="primary"
|
||||
loading={confirmPasswordMutation.isPending}
|
||||
onClick={(e) => onConfirmPassword(e)}>
|
||||
@@ -405,13 +454,50 @@ const ProfileView = () => {
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{showSecondaryEmailModalOpen && (
|
||||
<SecondaryEmailModal
|
||||
isLoading={addSecondaryEmailMutation.isPending}
|
||||
errorMessage={secondaryEmailAddErrorMessage}
|
||||
handleAddEmail={(values) => {
|
||||
setSecondaryEmailAddErrorMessage("");
|
||||
addSecondaryEmailMutation.mutate(values);
|
||||
}}
|
||||
onCancel={() => {
|
||||
setSecondaryEmailAddErrorMessage("");
|
||||
setShowSecondaryEmailModalOpen(false);
|
||||
}}
|
||||
clearErrorMessage={() => {
|
||||
addSecondaryEmailMutation.reset();
|
||||
setSecondaryEmailAddErrorMessage("");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{!!newlyAddedSecondaryEmail && (
|
||||
<SecondaryEmailConfirmModal
|
||||
email={newlyAddedSecondaryEmail}
|
||||
onCancel={() => setNewlyAddedSecondaryEmail(undefined)}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type SecondaryEmailApiPayload = {
|
||||
id: number;
|
||||
email: string;
|
||||
isDeleted: boolean;
|
||||
};
|
||||
|
||||
type ExtendedFormValues = Omit<FormValues, "secondaryEmails"> & {
|
||||
secondaryEmails: SecondaryEmailApiPayload[];
|
||||
};
|
||||
|
||||
const ProfileForm = ({
|
||||
defaultValues,
|
||||
onSubmit,
|
||||
handleAddSecondaryEmail,
|
||||
handleResendVerifyEmail,
|
||||
extraField,
|
||||
isPending = false,
|
||||
isFallbackImg,
|
||||
@@ -419,7 +505,9 @@ const ProfileForm = ({
|
||||
userOrganization,
|
||||
}: {
|
||||
defaultValues: FormValues;
|
||||
onSubmit: (values: FormValues) => void;
|
||||
onSubmit: (values: ExtendedFormValues) => void;
|
||||
handleAddSecondaryEmail: () => void;
|
||||
handleResendVerifyEmail: (email: string) => void;
|
||||
extraField?: React.ReactNode;
|
||||
isPending: boolean;
|
||||
isFallbackImg: boolean;
|
||||
@@ -441,6 +529,14 @@ const ProfileForm = ({
|
||||
}),
|
||||
email: z.string().email(),
|
||||
bio: z.string(),
|
||||
secondaryEmails: z.array(
|
||||
z.object({
|
||||
id: z.number(),
|
||||
email: z.string().email(),
|
||||
emailVerified: z.union([z.string(), z.null()]).optional(),
|
||||
emailPrimary: z.boolean().optional(),
|
||||
})
|
||||
),
|
||||
});
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
@@ -448,13 +544,61 @@ const ProfileForm = ({
|
||||
resolver: zodResolver(profileFormSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
fields: secondaryEmailFields,
|
||||
remove: deleteSecondaryEmail,
|
||||
replace: updateAllSecondaryEmailFields,
|
||||
} = useFieldArray({
|
||||
control: formMethods.control,
|
||||
name: "secondaryEmails",
|
||||
keyName: "itemId",
|
||||
});
|
||||
|
||||
const handleFormSubmit = (values: FormValues) => {
|
||||
const changedFields = formMethods.formState.dirtyFields?.secondaryEmails || [];
|
||||
const updatedValues: FormValues = {
|
||||
...values,
|
||||
};
|
||||
|
||||
// If the primary email is changed, we will need to update
|
||||
const primaryEmailIndex = updatedValues.secondaryEmails.findIndex(
|
||||
(secondaryEmail) => secondaryEmail.emailPrimary
|
||||
);
|
||||
if (primaryEmailIndex >= 0) {
|
||||
// Add the new updated value as primary email
|
||||
updatedValues.email = updatedValues.secondaryEmails[primaryEmailIndex].email;
|
||||
}
|
||||
|
||||
// We will only send the emails which have already changed
|
||||
const updatedEmails: Email[] = [];
|
||||
changedFields.map((field, index) => {
|
||||
// If the email changed and if its only secondary email, we add it for updation, the first
|
||||
// item in the list is always primary email
|
||||
if (field?.email && updatedValues.secondaryEmails[index]?.id) {
|
||||
updatedEmails.push(updatedValues.secondaryEmails[index]);
|
||||
}
|
||||
});
|
||||
|
||||
const deletedEmails = (user?.secondaryEmails || []).filter(
|
||||
(secondaryEmail) => !updatedValues.secondaryEmails.find((val) => val.id && val.id === secondaryEmail.id)
|
||||
);
|
||||
const secondaryEmails = [
|
||||
...updatedEmails.map((email) => ({ ...email, isDeleted: false })),
|
||||
...deletedEmails.map((email) => ({ ...email, isDeleted: true })),
|
||||
].map((secondaryEmail) => pick(secondaryEmail, ["id", "email", "isDeleted"]));
|
||||
onSubmit({
|
||||
...updatedValues,
|
||||
secondaryEmails,
|
||||
});
|
||||
};
|
||||
|
||||
const {
|
||||
formState: { isSubmitting, isDirty },
|
||||
} = formMethods;
|
||||
|
||||
const isDisabled = isSubmitting || !isDirty;
|
||||
return (
|
||||
<Form form={formMethods} handleSubmit={onSubmit}>
|
||||
<Form form={formMethods} handleSubmit={handleFormSubmit}>
|
||||
<div className="border-subtle border-x px-4 pb-10 pt-8 sm:px-6">
|
||||
<div className="flex items-center">
|
||||
<Controller
|
||||
@@ -508,12 +652,36 @@ const ProfileForm = ({
|
||||
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<TextField
|
||||
label={t("email")}
|
||||
hint={t("change_email_hint")}
|
||||
data-testId="profile-form-email"
|
||||
{...formMethods.register("email")}
|
||||
/>
|
||||
<Label>{t("email")}</Label>
|
||||
{secondaryEmailFields.map((field, index) => (
|
||||
<CustomEmailTextField
|
||||
key={field.itemId}
|
||||
formMethods={formMethods}
|
||||
formMethodFieldName={`secondaryEmails.${index}.email` as keyof FormValues}
|
||||
errorMessage={get(formMethods.formState.errors, `secondaryEmails.${index}.email.message`)}
|
||||
emailVerified={Boolean(field.emailVerified)}
|
||||
emailPrimary={field.emailPrimary}
|
||||
dataTestId={`profile-form-email-${index}`}
|
||||
handleChangePrimary={() => {
|
||||
const fields = secondaryEmailFields.map((secondaryField, cIndex) => ({
|
||||
...secondaryField,
|
||||
emailPrimary: cIndex === index,
|
||||
}));
|
||||
updateAllSecondaryEmailFields(fields);
|
||||
}}
|
||||
handleVerifyEmail={() => handleResendVerifyEmail(field.email)}
|
||||
handleItemDelete={() => deleteSecondaryEmail(index)}
|
||||
/>
|
||||
))}
|
||||
<div className="text-default mt-2 flex items-center text-sm">{t("change_email_hint")}</div>
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={Plus}
|
||||
className="mt-2"
|
||||
onClick={() => handleAddSecondaryEmail()}
|
||||
data-testid="add-secondary-email">
|
||||
{t("add_email")}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="mt-6">
|
||||
<Label>{t("about")}</Label>
|
||||
@@ -535,7 +703,7 @@ const ProfileForm = ({
|
||||
disabled={isDisabled}
|
||||
color="primary"
|
||||
type="submit"
|
||||
data-testId="profile-submit-button">
|
||||
data-testid="profile-submit-button">
|
||||
{t("update")}
|
||||
</Button>
|
||||
</SectionBottomActions>
|
||||
|
||||
@@ -159,6 +159,7 @@ export default function Signup({
|
||||
isGoogleLoginEnabled,
|
||||
isSAMLLoginEnabled,
|
||||
orgAutoAcceptEmail,
|
||||
redirectUrl,
|
||||
}: SignupProps) {
|
||||
const [premiumUsername, setPremiumUsername] = useState(false);
|
||||
const [usernameTaken, setUsernameTaken] = useState(false);
|
||||
@@ -180,6 +181,12 @@ export default function Signup({
|
||||
formState: { isSubmitting, errors, isSubmitSuccessful },
|
||||
} = formMethods;
|
||||
|
||||
useEffect(() => {
|
||||
if (redirectUrl) {
|
||||
localStorage.setItem("onBoardingRedirect", redirectUrl);
|
||||
}
|
||||
}, [redirectUrl]);
|
||||
|
||||
const loadingSubmitState = isSubmitSuccessful || isSubmitting;
|
||||
|
||||
const handleErrorsAndStripe = async (resp: Response) => {
|
||||
|
||||
@@ -13,7 +13,7 @@ import { useEffect } from "react";
|
||||
|
||||
import { sdkActionManager, useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getOrgAvatarUrl, getTeamAvatarUrl } from "@calcom/lib/getAvatarUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
@@ -62,7 +62,7 @@ function TeamPage({
|
||||
return (
|
||||
<div className="flex h-full min-h-[100dvh] items-center justify-center">
|
||||
<UnpublishedEntity
|
||||
{...(metadata?.isOrganization || team.parentId ? { orgSlug: slug } : { teamSlug: slug })}
|
||||
{...(team?.isOrganization || team.parentId ? { orgSlug: slug } : { teamSlug: slug })}
|
||||
name={teamName}
|
||||
/>
|
||||
</div>
|
||||
@@ -123,7 +123,7 @@ function TeamPage({
|
||||
(mem) => mem.subteams?.includes(ch.slug) && mem.accepted
|
||||
).length;
|
||||
return (
|
||||
<li key={i} className="hover:bg-muted w-full">
|
||||
<li key={i} className="hover:bg-muted w-full rounded-md">
|
||||
<Link href={`/${ch.slug}`} className="flex items-center justify-between">
|
||||
<div className="flex items-center px-5 py-5">
|
||||
<div className="ms-3 inline-block truncate">
|
||||
@@ -157,6 +157,11 @@ function TeamPage({
|
||||
</div>
|
||||
);
|
||||
|
||||
const profileImageSrc =
|
||||
isValidOrgDomain || team.isOrganization
|
||||
? getOrgAvatarUrl({ slug: currentOrgDomain, logoUrl: team.logoUrl })
|
||||
: getTeamAvatarUrl({ slug: team.slug, logoUrl: team.logoUrl, organizationId: team.parent?.id });
|
||||
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
@@ -166,22 +171,14 @@ function TeamPage({
|
||||
title: markdownStrippedBio,
|
||||
profile: {
|
||||
name: `${team.name}`,
|
||||
image: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`,
|
||||
image: profileImageSrc,
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<main className="dark:bg-darkgray-50 bg-subtle mx-auto max-w-3xl rounded-md px-4 pb-12 pt-12">
|
||||
<div className="mx-auto mb-8 max-w-3xl text-center">
|
||||
<div className="relative">
|
||||
<Avatar
|
||||
alt={teamName}
|
||||
imageSrc={
|
||||
isValidOrgDomain
|
||||
? `/org/${currentOrgDomain}/avatar.png`
|
||||
: `${WEBAPP_URL}/${team.metadata?.isOrganization ? "org" : "team"}/${team.slug}/avatar.png`
|
||||
}
|
||||
size="lg"
|
||||
/>
|
||||
<Avatar alt={teamName} imageSrc={profileImageSrc} size="lg" />
|
||||
</div>
|
||||
<p className="font-cal text-emphasis mb-2 text-2xl tracking-wider" data-testid="team-name">
|
||||
{team.parent && `${team.parent.name} `}
|
||||
@@ -196,7 +193,7 @@ function TeamPage({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{metadata?.isOrganization ? (
|
||||
{team.isOrganization ? (
|
||||
<SubTeams />
|
||||
) : (
|
||||
<>
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { loginUser } from "../../fixtures/regularBookings";
|
||||
import { test } from "../../lib/fixtures";
|
||||
|
||||
const ALL_APPS = ["fathom", "matomo", "plausible", "ga4", "gtm", "metapixel"];
|
||||
|
||||
test.describe("Check analytics Apps", () => {
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/apps/");
|
||||
});
|
||||
|
||||
test("Check analytics Apps", async ({ appsPage, page }) => {
|
||||
await appsPage.goToAppsCategory("analytics");
|
||||
await appsPage.installApp("fathom");
|
||||
await appsPage.goBackToAppsPage();
|
||||
await appsPage.installApp("matomo");
|
||||
await appsPage.goBackToAppsPage();
|
||||
await appsPage.installApp("plausible");
|
||||
await appsPage.goBackToAppsPage();
|
||||
await appsPage.installApp("ga4");
|
||||
await appsPage.goBackToAppsPage();
|
||||
await appsPage.installApp("gtm");
|
||||
await appsPage.goBackToAppsPage();
|
||||
await appsPage.installApp("metapixel");
|
||||
await appsPage.goBackToAppsPage();
|
||||
await page.goto("/event-types");
|
||||
await appsPage.goToEventType("30 min");
|
||||
await appsPage.goToAppsTab();
|
||||
await appsPage.verifyAppsInfo(0);
|
||||
for (const app of ALL_APPS) {
|
||||
await appsPage.activeApp(app);
|
||||
}
|
||||
await appsPage.verifyAppsInfo(6);
|
||||
});
|
||||
});
|
||||
@@ -86,7 +86,8 @@ testBothFutureAndLegacyRoutes.describe("Forgot password", async () => {
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
expect(await verifyPassword(newPassword, updatedUser.password!)).toBeTruthy();
|
||||
const updatedPassword = updatedUser.password!.hash;
|
||||
expect(await verifyPassword(newPassword, updatedPassword)).toBeTruthy();
|
||||
|
||||
// finally, make sure the same URL cannot be used to reset the password again, as it should be expired.
|
||||
await page.goto(`/auth/forgot-password/${id}`);
|
||||
|
||||
@@ -175,6 +175,35 @@ testBothFutureAndLegacyRoutes.describe("pro user", () => {
|
||||
await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test("can book an unconfirmed event multiple times", async ({ page, users }) => {
|
||||
await page.locator('[data-testid="event-type-link"]:has-text("Opt in")').click();
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
const pageUrl = page.url();
|
||||
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
// go back to the booking page to re-book.
|
||||
await page.goto(pageUrl);
|
||||
await bookTimeSlot(page, { email: "test2@example.com" });
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
});
|
||||
|
||||
test("cannot book an unconfirmed event multiple times with the same email", async ({ page, users }) => {
|
||||
await page.locator('[data-testid="event-type-link"]:has-text("Opt in")').click();
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
const pageUrl = page.url();
|
||||
|
||||
await bookTimeSlot(page);
|
||||
// go back to the booking page to re-book.
|
||||
await page.goto(pageUrl);
|
||||
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.getByText("Could not book the meeting.")).toBeVisible();
|
||||
});
|
||||
|
||||
test("can book with multiple guests", async ({ page, users }) => {
|
||||
const additionalGuests = ["test@gmail.com", "test2@gmail.com"];
|
||||
|
||||
|
||||
@@ -1,483 +0,0 @@
|
||||
import { loginUser } from "../../fixtures/regularBookings";
|
||||
import { test } from "../../lib/fixtures";
|
||||
|
||||
test.describe("Booking With Address Question and Each Other Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and Checkbox Group Question", () => {
|
||||
test("Address required and checkbox group required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Checkbox Group question (both required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Address and checkbox group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Checkbox Group question (only address required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and Checkbox Question", () => {
|
||||
test("Address required and checkbox required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Checkbox question (both required)",
|
||||
secondQuestion: "boolean",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Addres and checkbox not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Checkbox question (only address required)",
|
||||
secondQuestion: "boolean",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and Long text Question", () => {
|
||||
test("Addres required and Long Text required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Long Text question (both required)",
|
||||
secondQuestion: "textarea",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Address and Long Text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Long Text question (only address required)",
|
||||
secondQuestion: "textarea",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and Multi email Question", () => {
|
||||
test("Address required and Multi email required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Multiemail question (both required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Address and Multi email not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
false,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Multiemail question (only address required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and multiselect Question", () => {
|
||||
test("Address required and multiselect text required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Multi Select question (both required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { ...bookingOptions, isMultiSelect: true },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Address and multiselect text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Multi Select question (only address required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { ...bookingOptions, isMultiSelect: true, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and Number Question", () => {
|
||||
test("Address required and Number required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Number question (both required)",
|
||||
secondQuestion: "number",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Address and Number not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Number question (only address required)",
|
||||
secondQuestion: "number",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and Phone Question", () => {
|
||||
test("Address required and Phone required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone-test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Multi Select question (both required)",
|
||||
secondQuestion: "phone",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Address and Phone not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone-test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Multi Select question (only address required)",
|
||||
secondQuestion: "phone",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and Radio group Question", () => {
|
||||
test("Address required and Radio group required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Radio question (both required)",
|
||||
secondQuestion: "radio",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Address and Radio group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Radio question (only address required)",
|
||||
secondQuestion: "radio",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and select Question", () => {
|
||||
test("Address required and select required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Select question (both required)",
|
||||
secondQuestion: "select",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Address and select not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Select question (both required)",
|
||||
secondQuestion: "select",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Address Question and Short text question", () => {
|
||||
test("Address required and Short text required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Multi Select question (both required)",
|
||||
secondQuestion: "text",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Address and Short text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "address",
|
||||
fillText: "Test Address question and Multi Select question (only address required)",
|
||||
secondQuestion: "text",
|
||||
options: { ...bookingOptions, isRequired: true },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
/* eslint-disable playwright/no-conditional-in-test */
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("Booking With All Questions", () => {
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
const bookingOptions = { isAllRequired: true };
|
||||
|
||||
test("Selecting and filling all questions as required", async ({ bookingPage }) => {
|
||||
const allQuestions = [
|
||||
"phone",
|
||||
"address",
|
||||
"checkbox",
|
||||
"boolean",
|
||||
"textarea",
|
||||
"multiemail",
|
||||
"multiselect",
|
||||
"number",
|
||||
"radio",
|
||||
"select",
|
||||
"text",
|
||||
];
|
||||
for (const question of allQuestions) {
|
||||
if (
|
||||
question !== "number" &&
|
||||
question !== "select" &&
|
||||
question !== "checkbox" &&
|
||||
question !== "boolean" &&
|
||||
question !== "multiselect" &&
|
||||
question !== "radio"
|
||||
) {
|
||||
await bookingPage.addQuestion(
|
||||
question,
|
||||
`${question}-test`,
|
||||
`${question} test`,
|
||||
true,
|
||||
`${question} test`
|
||||
);
|
||||
} else {
|
||||
await bookingPage.addQuestion(question, `${question}-test`, `${question} test`, true);
|
||||
}
|
||||
await bookingPage.checkField(question);
|
||||
}
|
||||
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAllQuestions(eventTypePage, allQuestions, bookingOptions);
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Selecting and filling all questions as optional", async ({ bookingPage }) => {
|
||||
const allQuestions = [
|
||||
"phone",
|
||||
"address",
|
||||
"checkbox",
|
||||
"boolean",
|
||||
"textarea",
|
||||
"multiemail",
|
||||
"multiselect",
|
||||
"number",
|
||||
"radio",
|
||||
"select",
|
||||
"text",
|
||||
];
|
||||
for (const question of allQuestions) {
|
||||
if (
|
||||
question !== "number" &&
|
||||
question !== "select" &&
|
||||
question !== "checkbox" &&
|
||||
question !== "boolean" &&
|
||||
question !== "multiselect" &&
|
||||
question !== "radio"
|
||||
) {
|
||||
await bookingPage.addQuestion(
|
||||
question,
|
||||
`${question}-test`,
|
||||
`${question} test`,
|
||||
false,
|
||||
`${question} test`
|
||||
);
|
||||
} else {
|
||||
await bookingPage.addQuestion(question, `${question}-test`, `${question} test`, false);
|
||||
}
|
||||
await bookingPage.checkField(question, { isOptional: true });
|
||||
}
|
||||
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAllQuestions(eventTypePage, allQuestions, {
|
||||
...bookingOptions,
|
||||
isAllRequired: false,
|
||||
});
|
||||
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
@@ -1,445 +0,0 @@
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and Each Other Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and Address Question", () => {
|
||||
test("Checkbox Group required and Address required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Address question (both required)",
|
||||
secondQuestion: "address",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and Address not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Address question (only checkbox required)",
|
||||
secondQuestion: "address",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and Phone Question", () => {
|
||||
test("Checkbox Group required and Phone required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and phone question (both required)",
|
||||
secondQuestion: "phone",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and Phone not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Phone question (only checkbox required)",
|
||||
secondQuestion: "phone",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and checkbox Question", () => {
|
||||
test("Checkbox Group required and checkbox required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and checkbox question (both required)",
|
||||
secondQuestion: "boolean",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and checkbox not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and checkbox (only checkbox required)",
|
||||
secondQuestion: "boolean",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and Long text Question", () => {
|
||||
test("Checkbox Group required and Long text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Long Text question (both required)",
|
||||
secondQuestion: "textarea",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and Long text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Long Text question (only checkbox required)",
|
||||
secondQuestion: "textarea",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and Multi email Question", () => {
|
||||
test("Checkbox Group required and Multi email required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Multi Email question (both required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and Multi email not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
false,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Multi Email question (only checkbox required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and multiselect Question", () => {
|
||||
test("Checkbox Group required and multiselect text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Multi Select question (both required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and multiselect text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Multi Select question (only checkbox required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and Number Question", () => {
|
||||
test("Checkbox Group required and Number required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Number question (both required)",
|
||||
secondQuestion: "number",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and Number not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Number question (only checkbox required)",
|
||||
secondQuestion: "number",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and Radio group Question", () => {
|
||||
test("Checkbox Group required and Radio group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Radio question (both required)",
|
||||
secondQuestion: "radio",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and Radio group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Radio question (only checkbox required)",
|
||||
secondQuestion: "radio",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and select Question", () => {
|
||||
test("Checkbox Group required and select required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Select question (both required)",
|
||||
secondQuestion: "select",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and select not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Select question (only checkbox required)",
|
||||
secondQuestion: "select",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Checkbox Group Question and Short text question", () => {
|
||||
test("Checkbox Group required and Short Text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Text question (both required)",
|
||||
secondQuestion: "text",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Checkbox Group and Short Text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "checkbox",
|
||||
fillText: "Test Checkbox Group question and Text question (only checkbox required)",
|
||||
secondQuestion: "text",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,54 +0,0 @@
|
||||
import { test } from "../../lib/fixtures";
|
||||
|
||||
test.describe("Collective event type", () => {
|
||||
const teamEventTitle = "Test Managed Event Type";
|
||||
let userId: number;
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
const userFixture = await users.create(
|
||||
{ name: "testuser" },
|
||||
{ hasTeam: true, schedulingType: "COLLECTIVE", teamEventTitle }
|
||||
);
|
||||
await userFixture.apiLogin();
|
||||
userId = userFixture.id;
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType(teamEventTitle);
|
||||
});
|
||||
test("Book a Collective event type", async ({ bookingPage }) => {
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillEmailAndName(eventTypePage);
|
||||
await bookingPage.confirmBooking(eventTypePage);
|
||||
await bookingPage.backToBookings(eventTypePage);
|
||||
await bookingPage.assertLabelWithCorrectTeamName(eventTypePage, teamEventTitle);
|
||||
await bookingPage.assertBookingWithCorrectTitleAndDescription(eventTypePage, {
|
||||
profileName: "testuser",
|
||||
bookingName: teamEventTitle,
|
||||
teamName: `user-id-${userId}'s Team`,
|
||||
});
|
||||
await bookingPage.clickOnBooking(eventTypePage, teamEventTitle);
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBookingWithReason(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
test("Book a Collective event type (with added guest)", async ({ bookingPage }) => {
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillEmailAndName(eventTypePage);
|
||||
await bookingPage.addGuests(eventTypePage, { guests: ["test@example.com"] });
|
||||
await bookingPage.confirmBooking(eventTypePage);
|
||||
await bookingPage.backToBookings(eventTypePage);
|
||||
await bookingPage.assertLabelWithCorrectTeamName(eventTypePage, teamEventTitle);
|
||||
await bookingPage.assertBookingWithCorrectTitleAndDescription(eventTypePage, {
|
||||
profileName: "testuser",
|
||||
bookingName: teamEventTitle,
|
||||
teamName: `user-id-${userId}'s Team`,
|
||||
aditionalGuestEmail: "test@example.com",
|
||||
});
|
||||
await bookingPage.clickOnBooking(eventTypePage, teamEventTitle);
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBookingWithReason(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
@@ -1,483 +0,0 @@
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("Booking With Long Text Question and Each Other Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
|
||||
test.beforeEach(async ({ page, users }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
});
|
||||
|
||||
test("Long Text and Address required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Address question (both required)",
|
||||
secondQuestion: "address",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Long Text required and Address not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Address question (only Long Text required)",
|
||||
secondQuestion: "address",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test.describe("Booking With Long Text Question and Checkbox Group Question", () => {
|
||||
test("Long Text and Checkbox Group required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Checkbox Group question (both required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Long Text required and Checkbox Group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Checkbox Group question (only Long Text required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Long Text Question and checkbox Question", () => {
|
||||
test("Long Text and checkbox required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Checkbox question (only Long Text required)",
|
||||
secondQuestion: "boolean",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Long Text required and checkbox not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Checkbox question (only Long Text required)",
|
||||
secondQuestion: "boolean",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Long Text Question and Multiple email Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
test("Long Text and Multiple email required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Multiple email question (both required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Long Text required and Multiple email not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
false,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Multiple email question (only Long Text required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: { hasPlaceholder: true, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Long Text Question and multiselect Question", () => {
|
||||
test("Long Text and multiselect text required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and multiselect question (both required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Long Text required and multiselect text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and multiselect question (only long text required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { hasPlaceholder: false, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Long Text Question and Number Question", () => {
|
||||
test("Long Text and Number required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Number question (both required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test("Long Text required and Number not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Number question (only Long Textß required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { hasPlaceholder: false, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test.describe("Booking With Long Text Question and Phone Question", () => {
|
||||
test("Long Text and Phone required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Phone question (both required)",
|
||||
secondQuestion: "phone",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Long Text required and Phone not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Phone question (only Long Text required)",
|
||||
secondQuestion: "phone",
|
||||
options: { hasPlaceholder: false, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Long Text Question and Radio group Question", () => {
|
||||
test("Long Text and Radio group required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Radio Group question (both required)",
|
||||
secondQuestion: "radio",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Long Text required and Radio group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Radio Group question (only Long Text required)",
|
||||
secondQuestion: "radio",
|
||||
options: { hasPlaceholder: false, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Long Text Question and select Question", () => {
|
||||
test("Long Text and select required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Select question (both required)",
|
||||
secondQuestion: "select",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Long Text required and select not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Select question (only Long Text required)",
|
||||
secondQuestion: "select",
|
||||
options: { hasPlaceholder: false, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Long Text Question and Short text question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
test("Long Text and Short text required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Text question (both required)",
|
||||
secondQuestion: "text",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Long Text required and Short text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "textarea",
|
||||
fillText: "Test Long Text question and Text question (only Long Text required)",
|
||||
secondQuestion: "text",
|
||||
options: { hasPlaceholder: false, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,553 +0,0 @@
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("Booking With Multiple Email Question and Each Other Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple Email Question and Address Question", () => {
|
||||
test("Multiple Email required and Address required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Address question (both required)",
|
||||
secondQuestion: "address",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple Email and Address not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Address question (only Multiple Email required)",
|
||||
secondQuestion: "address",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple Email Question and checkbox group Question", () => {
|
||||
test("Multiple Email required and checkbox group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and checkbox group question (both required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple Email and checkbox group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and checkbox group question (only Multiple Email required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple Email Question and checkbox Question", () => {
|
||||
test("Multiple Email required and checkbox required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and checkbox question (both required)",
|
||||
secondQuestion: "boolean",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple Email and checkbox not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and checkbox (only Multiple Email required)",
|
||||
secondQuestion: "boolean",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple Email Question and Long text Question", () => {
|
||||
test("Multiple Email required and Long text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Long Text question (both required)",
|
||||
secondQuestion: "textarea",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple Email and Long text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Long Text question (only Multiple Email required)",
|
||||
secondQuestion: "textarea",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple Email Question and multiselect Question", () => {
|
||||
test("Multiple Email required and multiselect text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Multi Select question (both required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple Email and multiselect text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Multi Select question (only Multiple Email required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple Email Question and Number Question", () => {
|
||||
test("Multiple Email required and Number required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Number question (both required)",
|
||||
secondQuestion: "number",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple Email and Number not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Number question (only Multiple Email required)",
|
||||
secondQuestion: "number",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple email Question and phone Question", () => {
|
||||
test("Multiple email required and Phone required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Phone question (both required)",
|
||||
secondQuestion: "phone",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple email and Phone not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Phone question (both required)",
|
||||
secondQuestion: "phone",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple Email Question and Radio group Question", () => {
|
||||
test("Multiple Email required and Radio group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Radio question (both required)",
|
||||
secondQuestion: "radio",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple Email and Radio group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Radio question (only Multiple Email required)",
|
||||
secondQuestion: "radio",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple Email Question and select Question", () => {
|
||||
test("Multiple Email required and select required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Select question (both required)",
|
||||
secondQuestion: "select",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple Email and select not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Select question (only Multiple Email required)",
|
||||
secondQuestion: "select",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Multiple Email Question and Short text question", () => {
|
||||
test("Multiple Email required and Short text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Text question (both required)",
|
||||
secondQuestion: "text",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Multiple Email and Short text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "multiemail",
|
||||
fillText: "Test Multiple Email question and Text question (only Multiple Email required)",
|
||||
secondQuestion: "text",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,446 +0,0 @@
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("Booking With Phone Question and Each Other Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and Address Question", () => {
|
||||
test("Phone and Address required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Address question (both required)",
|
||||
secondQuestion: "address",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Phone required and Address not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Address question (only phone required)",
|
||||
secondQuestion: "address",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and checkbox group Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: false, isRequired: true };
|
||||
test("Phone and checkbox group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and checkbox group question (both required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Phone required and checkbox group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and checkbox group question (only phone required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and checkbox Question", () => {
|
||||
test("Phone and checkbox required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and checkbox question (both required)",
|
||||
secondQuestion: "boolean",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
test("Phone required and checkbox not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and checkbox (only phone required)",
|
||||
secondQuestion: "boolean",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and Long text Question", () => {
|
||||
test("Phone and Long text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Long Text question (both required)",
|
||||
secondQuestion: "textarea",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Phone required and Long text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Long Text question (only phone required)",
|
||||
secondQuestion: "textarea",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and Multi email Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
test("Phone and Multi email required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Multi Email question (both required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Phone required and Multi email not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
false,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Multi Email question (only phone required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and multiselect Question", () => {
|
||||
test("Phone and multiselect text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Multi Select question (both required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Phone required and multiselect text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Multi Select question (only phone required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and Number Question", () => {
|
||||
test("Phone and Number required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Number question (both required)",
|
||||
secondQuestion: "number",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Phone required and Number not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Number question (only phone required)",
|
||||
secondQuestion: "number",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and Radio group Question", () => {
|
||||
test("Phone and Radio group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Radio question (both required)",
|
||||
secondQuestion: "radio",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Phone required and Radio group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Radio question (only phone required)",
|
||||
secondQuestion: "radio",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and select Question", () => {
|
||||
test("Phone and select required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Select question (both required)",
|
||||
secondQuestion: "select",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Phone required and select not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Select question (only phone required)",
|
||||
secondQuestion: "select",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Phone Question and Short text question", () => {
|
||||
test("Phone and Short text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Text question (both required)",
|
||||
secondQuestion: "text",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Phone required and Short text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "phone",
|
||||
fillText: "Test Phone question and Text question (only phone required)",
|
||||
secondQuestion: "text",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,444 +0,0 @@
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("Booking With Radio Question and Each Other Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and Address Question", () => {
|
||||
test("Radio required and Address required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Address question (both required)",
|
||||
secondQuestion: "address",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Radio and Address not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Address question (only radio required)",
|
||||
secondQuestion: "address",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and checkbox group Question", () => {
|
||||
test("Radio required and checkbox group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and checkbox group question (both required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Radio and checkbox group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and checkbox group question (only radio required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and checkbox Question", () => {
|
||||
test("Radio required and checkbox required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and checkbox question (both required)",
|
||||
secondQuestion: "boolean",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
test("Radio and checkbox not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and checkbox (only radio required)",
|
||||
secondQuestion: "boolean",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and Long text Question", () => {
|
||||
test("Radio required and Long text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Long Text question (both required)",
|
||||
secondQuestion: "textarea",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Radio and Long text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Long Text question (only radio required)",
|
||||
secondQuestion: "textarea",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and Multi email Question", () => {
|
||||
test("Radio required and Multi email required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Multi Email question (both required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Radio and Multi email not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
false,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Multi Email question (only radio required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and multiselect Question", () => {
|
||||
test("Radio required and multiselect text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Multi Select question (both required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Radio and multiselect text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Multi Select question (only radio required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and Number Question", () => {
|
||||
test("Radio required and Number required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Number question (both required)",
|
||||
secondQuestion: "number",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Radio and Number not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Number question (only radio required)",
|
||||
secondQuestion: "number",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and Phone Question", () => {
|
||||
test("Radio required and Phone required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Phone question (both required)",
|
||||
secondQuestion: "phone",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Radio and Phone not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Phone question (only radio required)",
|
||||
secondQuestion: "phone",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and select Question", () => {
|
||||
test("Radio required and select required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Select question (both required)",
|
||||
secondQuestion: "select",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Radio and select not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", false, "select test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Select question (only radio required)",
|
||||
secondQuestion: "select",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Radio Question and Short text question", () => {
|
||||
test("Radio required and Short text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Text question (both required)",
|
||||
secondQuestion: "text",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Radio and Short text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "radio",
|
||||
fillText: "Test Radio question and Text question (only radio required)",
|
||||
secondQuestion: "text",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,28 +0,0 @@
|
||||
/* eslint-disable playwright/no-conditional-in-test */
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe.configure({ mode: "serial" });
|
||||
|
||||
test.describe("Booking with recurring checked", () => {
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("recurring");
|
||||
});
|
||||
|
||||
test("Updates event type with recurring events", async ({ page, bookingPage }) => {
|
||||
await bookingPage.updateRecurringTab("2", "3");
|
||||
await bookingPage.updateEventType();
|
||||
await page.getByRole("link", { name: "Event Types" }).click();
|
||||
await bookingPage.assertRepeatEventType();
|
||||
});
|
||||
|
||||
test("Updates and shows recurring schedule correctly in booking page", async ({ bookingPage }) => {
|
||||
await bookingPage.updateRecurringTab("2", "3");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.fillRecurringFieldAndConfirm(eventTypePage);
|
||||
});
|
||||
});
|
||||
@@ -1,107 +0,0 @@
|
||||
/* eslint-disable playwright/no-conditional-in-test */
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
const resolutions = [
|
||||
{
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
},
|
||||
{
|
||||
width: 1280,
|
||||
height: 720,
|
||||
},
|
||||
{
|
||||
width: 640,
|
||||
height: 480,
|
||||
},
|
||||
];
|
||||
|
||||
test.describe("Booking With All Questions", () => {
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
|
||||
const allQuestions = [
|
||||
"phone",
|
||||
"address",
|
||||
"checkbox",
|
||||
"boolean",
|
||||
"textarea",
|
||||
"multiemail",
|
||||
"multiselect",
|
||||
"number",
|
||||
"radio",
|
||||
"select",
|
||||
"text",
|
||||
];
|
||||
for (const question of allQuestions) {
|
||||
if (
|
||||
question !== "number" &&
|
||||
question !== "select" &&
|
||||
question !== "checkbox" &&
|
||||
question !== "boolean" &&
|
||||
question !== "multiselect" &&
|
||||
question !== "radio"
|
||||
) {
|
||||
await bookingPage.addQuestion(
|
||||
question,
|
||||
`${question}-test`,
|
||||
`${question} test`,
|
||||
true,
|
||||
`${question} test`
|
||||
);
|
||||
} else {
|
||||
await bookingPage.addQuestion(question, `${question}-test`, `${question} test`, true);
|
||||
}
|
||||
await bookingPage.checkField(question);
|
||||
}
|
||||
});
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
test(`Booking with ${resolution.width}x${resolution.height} resolution and all questions`, async ({
|
||||
bookingPage,
|
||||
}) => {
|
||||
await bookingPage.setResolution(resolution.width, resolution.height);
|
||||
await bookingPage.updateEventType();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe("Booking With no questions", () => {
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
test(`Booking with ${resolution.width}x${resolution.height} resolution and no questions`, async ({
|
||||
bookingPage,
|
||||
}) => {
|
||||
await bookingPage.setResolution(resolution.width, resolution.height);
|
||||
await bookingPage.updateEventType();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test.describe("Booking page with no questions", () => {
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
for (const resolution of resolutions) {
|
||||
test(`Booking page with ${resolution.width}x${resolution.height} resolution`, async ({ bookingPage }) => {
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await eventTypePage.setViewportSize({ width: resolution.width, height: resolution.height });
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.confirmBooking(eventTypePage);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,441 +0,0 @@
|
||||
import { loginUser } from "../fixtures/regularBookings";
|
||||
import { test } from "../lib/fixtures";
|
||||
|
||||
test.describe("Booking With Phone Question and Each Other Question", () => {
|
||||
const bookingOptions = { hasPlaceholder: true, isRequired: true };
|
||||
|
||||
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
await loginUser(users);
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType("30 min");
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Address Question", () => {
|
||||
test("Select required and Address required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", true, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and Address question (both required)",
|
||||
secondQuestion: "address",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Address not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("address", "address-test", "address test", false, "address test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and Address question (only select required)",
|
||||
secondQuestion: "address",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and checkbox group Question", () => {
|
||||
test("Select required and checkbox group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and Checkbox question (both required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and checkbox group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("checkbox", "checkbox-test", "checkbox test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and Checkbox question (only Select required)",
|
||||
secondQuestion: "checkbox",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and checkbox Question", () => {
|
||||
test("Select required and checkbox required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and boolean question (both required)",
|
||||
secondQuestion: "boolean",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and checkbox not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("boolean", "boolean-test", "boolean test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and boolean question (only select required)",
|
||||
secondQuestion: "boolean",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Long text Question", () => {
|
||||
test("Select required and Long text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", true, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and textarea question (both required)",
|
||||
secondQuestion: "textarea",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Long text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("textarea", "textarea-test", "textarea test", false, "textarea test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and textarea question (only select required)",
|
||||
secondQuestion: "textarea",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Multi email Question", () => {
|
||||
test("Select required and Multi email required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
true,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and multiemail question (both required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Multi email not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion(
|
||||
"multiemail",
|
||||
"multiemail-test",
|
||||
"multiemail test",
|
||||
false,
|
||||
"multiemail test"
|
||||
);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and multiemail question (only select required)",
|
||||
secondQuestion: "multiemail",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and multiselect Question", () => {
|
||||
test("Select required and multiselect text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and multiselect question (both required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and multiselect text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("multiselect", "multiselect-test", "multiselect test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and multiselect question (only select required)",
|
||||
secondQuestion: "multiselect",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Number Question", () => {
|
||||
test("Select required and Number required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", true, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and number question (both required)",
|
||||
secondQuestion: "number",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Number not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("number", "number-test", "number test", false, "number test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and number question (only select required)",
|
||||
secondQuestion: "number",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Phone Question", () => {
|
||||
test("Select required and select required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", true, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and phone question (both required)",
|
||||
secondQuestion: "phone",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Phone not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("phone", "phone-test", "phone test", false, "phone test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and phone question (only select required)",
|
||||
secondQuestion: "phone",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Radio group Question", () => {
|
||||
test("Select required and Radio group required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", true);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and radio question (both required)",
|
||||
secondQuestion: "radio",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Radio group not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("radio", "radio-test", "radio test", false);
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and radio question (only select required)",
|
||||
secondQuestion: "radio",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Booking With Select Question and Short text question", () => {
|
||||
test("Select required and Short text required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", true, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and text question (both required)",
|
||||
secondQuestion: "text",
|
||||
options: bookingOptions,
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
|
||||
test("Select and Short text not required", async ({ bookingPage }) => {
|
||||
await bookingPage.addQuestion("select", "select-test", "select test", true, "select test");
|
||||
await bookingPage.addQuestion("text", "text-test", "text test", false, "text test");
|
||||
await bookingPage.updateEventType();
|
||||
const eventTypePage = await bookingPage.previewEventType();
|
||||
await bookingPage.selectTimeSlot(eventTypePage);
|
||||
await bookingPage.fillAndConfirmBooking({
|
||||
eventTypePage,
|
||||
placeholderText: "Please share anything that will help prepare for our meeting.",
|
||||
question: "select",
|
||||
fillText: "Test Select question and text question (only select required)",
|
||||
secondQuestion: "text",
|
||||
options: { ...bookingOptions, isRequired: false },
|
||||
});
|
||||
await bookingPage.rescheduleBooking(eventTypePage);
|
||||
await bookingPage.assertBookingRescheduled(eventTypePage);
|
||||
await bookingPage.cancelBooking(eventTypePage);
|
||||
await bookingPage.assertBookingCanceled(eventTypePage);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,9 @@ import { expect } from "@playwright/test";
|
||||
|
||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { moveUserToOrg } from "@lib/orgMigration";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
@@ -110,4 +113,54 @@ test.describe("Change username on settings", () => {
|
||||
|
||||
await expect(page).toHaveURL(/.*checkout.stripe.com/);
|
||||
});
|
||||
|
||||
test("User can't take a username that has been migrated to a different username in an organization", async ({
|
||||
users,
|
||||
orgs,
|
||||
page,
|
||||
}) => {
|
||||
const existingUser =
|
||||
await test.step("Migrate user to a different username in an organization", async () => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
|
||||
const existingUser = await users.create({
|
||||
username: "john",
|
||||
emailDomain: org.organizationSettings?.orgAutoAcceptEmail ?? "",
|
||||
name: "John Outside Organization",
|
||||
});
|
||||
|
||||
await moveUserToOrg({
|
||||
user: existingUser,
|
||||
targetOrg: {
|
||||
// Changed username. After this there is no user with username equal to {existingUser.username}
|
||||
username: `${existingUser.username}-org`,
|
||||
id: org.id,
|
||||
membership: {
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
shouldMoveTeams: false,
|
||||
});
|
||||
return existingUser;
|
||||
});
|
||||
|
||||
await test.step("Changing username for another user to the previous username of migrated user - shouldn't be allowed", async () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const previousUsername = existingUser.username!;
|
||||
|
||||
const user = await users.create();
|
||||
await user.apiLogin();
|
||||
|
||||
await page.goto("/settings/my-account/profile");
|
||||
const usernameInput = page.locator("[data-testid=username-input]");
|
||||
|
||||
await usernameInput.fill(previousUsername);
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator("[data-testid=update-username-btn]").nth(0)).toBeHidden();
|
||||
await expect(page.locator("[data-testid=update-username-btn]").nth(1)).toBeHidden();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
export function createAppsFixture(page: Page) {
|
||||
return {
|
||||
goToAppsCategory: async (category: string) => {
|
||||
await page.getByTestId(`app-store-category-${category}`).nth(1).click();
|
||||
await page.goto("apps/categories/analytics");
|
||||
},
|
||||
installApp: async (app: string) => {
|
||||
await page.getByTestId(`app-store-app-card-${app}`).click();
|
||||
await page.getByTestId("install-app-button").click();
|
||||
},
|
||||
goBackToAppsPage: async () => {
|
||||
await page.getByTestId("add-apps").click();
|
||||
},
|
||||
goToEventType: async (eventType: string) => {
|
||||
await page.getByRole("link", { name: eventType }).click();
|
||||
},
|
||||
goToAppsTab: async () => {
|
||||
await page.getByTestId("vertical-tab-apps").click();
|
||||
},
|
||||
activeApp: async (app: string) => {
|
||||
await page.getByTestId(`${app}-app-switch`).click();
|
||||
},
|
||||
verifyAppsInfo: async (activeApps: number) => {
|
||||
await expect(page.locator(`text=6 apps, ${activeApps} active`)).toBeVisible();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -48,8 +48,8 @@ export async function createOrgInDb({
|
||||
data: {
|
||||
name: name,
|
||||
slug: slug,
|
||||
isOrganization: true,
|
||||
metadata: {
|
||||
isOrganization: true,
|
||||
...(requestedSlug
|
||||
? {
|
||||
requestedSlug,
|
||||
@@ -57,5 +57,8 @@ export async function createOrgInDb({
|
||||
: null),
|
||||
},
|
||||
},
|
||||
include: {
|
||||
organizationSettings: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,183 +1,14 @@
|
||||
import { expect, type Page } from "@playwright/test";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { localize } from "../lib/testUtils";
|
||||
import type { createUsersFixture } from "./users";
|
||||
|
||||
const reschedulePlaceholderText = "Let others know why you need to reschedule";
|
||||
export const scheduleSuccessfullyText = "This meeting is scheduled";
|
||||
|
||||
const EMAIL = "test@test.com";
|
||||
const EMAIL2 = "test2@test.com";
|
||||
const PHONE = "+55 (32) 983289947";
|
||||
|
||||
type BookingOptions = {
|
||||
hasPlaceholder?: boolean;
|
||||
isReschedule?: boolean;
|
||||
isRequired?: boolean;
|
||||
isAllRequired?: boolean;
|
||||
isMultiSelect?: boolean;
|
||||
};
|
||||
|
||||
interface QuestionActions {
|
||||
[key: string]: () => Promise<void>;
|
||||
}
|
||||
|
||||
type teamBookingtypes = { isManagedType?: boolean; isRoundRobinType?: boolean; isCollectiveType?: boolean };
|
||||
|
||||
type customLocators = {
|
||||
shouldChangeSelectLocator: boolean;
|
||||
shouldUseLastRadioGroupLocator: boolean;
|
||||
shouldUseFirstRadioGroupLocator: boolean;
|
||||
shouldChangeMultiSelectLocator: boolean;
|
||||
};
|
||||
|
||||
type fillAndConfirmBookingParams = {
|
||||
eventTypePage: Page;
|
||||
placeholderText: string;
|
||||
question: string;
|
||||
fillText: string;
|
||||
secondQuestion: string;
|
||||
options: BookingOptions;
|
||||
};
|
||||
|
||||
type UserFixture = ReturnType<typeof createUsersFixture>;
|
||||
|
||||
function isLastDayOfMonth(): boolean {
|
||||
const today = dayjs();
|
||||
const endOfMonth = today.endOf("month");
|
||||
return today.isSame(endOfMonth, "day");
|
||||
}
|
||||
|
||||
const fillQuestion = async (eventTypePage: Page, questionType: string, customLocators: customLocators) => {
|
||||
const questionActions: QuestionActions = {
|
||||
phone: async () => {
|
||||
await eventTypePage.locator('input[name="phone-test"]').clear();
|
||||
await eventTypePage.locator('input[name="phone-test"]').fill(PHONE);
|
||||
},
|
||||
multiemail: async () => {
|
||||
await eventTypePage.getByRole("button", { name: `${questionType} test` }).click();
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).fill(EMAIL);
|
||||
await eventTypePage.getByTestId("add-another-guest").last().click();
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).last().fill(EMAIL2);
|
||||
},
|
||||
checkbox: async () => {
|
||||
if (customLocators.shouldUseLastRadioGroupLocator || customLocators.shouldChangeMultiSelectLocator) {
|
||||
await eventTypePage.getByLabel("Option 1").last().click();
|
||||
await eventTypePage.getByLabel("Option 2").last().click();
|
||||
} else if (customLocators.shouldUseFirstRadioGroupLocator) {
|
||||
await eventTypePage.getByLabel("Option 1").first().click();
|
||||
await eventTypePage.getByLabel("Option 2").first().click();
|
||||
} else {
|
||||
await eventTypePage.getByLabel("Option 1").click();
|
||||
await eventTypePage.getByLabel("Option 2").click();
|
||||
}
|
||||
},
|
||||
multiselect: async () => {
|
||||
if (customLocators.shouldChangeMultiSelectLocator) {
|
||||
await eventTypePage.getByLabel("multi-select-dropdown").click();
|
||||
await eventTypePage.getByTestId("select-option-Option 1").click();
|
||||
} else {
|
||||
await eventTypePage.getByLabel("multi-select-dropdown").last().click();
|
||||
await eventTypePage.getByTestId("select-option-Option 1").click();
|
||||
}
|
||||
},
|
||||
boolean: async () => {
|
||||
await eventTypePage.getByLabel(`${questionType} test`).check();
|
||||
},
|
||||
radio: async () => {
|
||||
await eventTypePage.locator('[id="radio-test\\.option\\.0\\.radio"]').click();
|
||||
},
|
||||
select: async () => {
|
||||
if (customLocators.shouldChangeSelectLocator) {
|
||||
await eventTypePage.getByLabel("select-dropdown").first().click();
|
||||
await eventTypePage.getByTestId("select-option-Option 1").click();
|
||||
} else {
|
||||
await eventTypePage.getByLabel("select-dropdown").last().click();
|
||||
await eventTypePage.getByTestId("select-option-Option 1").click();
|
||||
}
|
||||
},
|
||||
number: async () => {
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("123");
|
||||
},
|
||||
address: async () => {
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("address test");
|
||||
},
|
||||
textarea: async () => {
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("textarea test");
|
||||
},
|
||||
text: async () => {
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).click();
|
||||
await eventTypePage.getByPlaceholder(`${questionType} test`).fill("text test");
|
||||
},
|
||||
};
|
||||
if (questionActions[questionType]) {
|
||||
await questionActions[questionType]();
|
||||
}
|
||||
};
|
||||
|
||||
const fillAllQuestions = async (eventTypePage: Page, questions: string[], options: BookingOptions) => {
|
||||
if (options.isAllRequired) {
|
||||
for (const question of questions) {
|
||||
switch (question) {
|
||||
case "email":
|
||||
await eventTypePage.getByPlaceholder("Email").click();
|
||||
await eventTypePage.getByPlaceholder("Email").fill(EMAIL);
|
||||
break;
|
||||
case "phone":
|
||||
await eventTypePage.getByPlaceholder("Phone test").click();
|
||||
await eventTypePage.getByPlaceholder("Phone test").fill(PHONE);
|
||||
break;
|
||||
case "address":
|
||||
await eventTypePage.getByPlaceholder("Address test").click();
|
||||
await eventTypePage.getByPlaceholder("Address test").fill("123 Main St, City, Country");
|
||||
break;
|
||||
case "textarea":
|
||||
await eventTypePage.getByPlaceholder("Textarea test").click();
|
||||
await eventTypePage.getByPlaceholder("Textarea test").fill("This is a sample text for textarea.");
|
||||
break;
|
||||
case "select":
|
||||
await eventTypePage.getByLabel("select-dropdown").last().click();
|
||||
await eventTypePage.getByTestId("select-option-Option 1").click();
|
||||
break;
|
||||
case "multiselect":
|
||||
// select-dropdown
|
||||
await eventTypePage.getByLabel("multi-select-dropdown").click();
|
||||
await eventTypePage.getByTestId("select-option-Option 1").click();
|
||||
break;
|
||||
case "number":
|
||||
await eventTypePage.getByLabel("number test").click();
|
||||
await eventTypePage.getByLabel("number test").fill("123");
|
||||
break;
|
||||
case "radio":
|
||||
await eventTypePage.getByRole("radiogroup").getByText("Option 1").check();
|
||||
break;
|
||||
case "text":
|
||||
await eventTypePage.getByPlaceholder("Text test").click();
|
||||
await eventTypePage.getByPlaceholder("Text test").fill("Sample text");
|
||||
break;
|
||||
case "checkbox":
|
||||
await eventTypePage.getByLabel("Option 1").first().check();
|
||||
await eventTypePage.getByLabel("Option 2").first().check();
|
||||
break;
|
||||
case "boolean":
|
||||
await eventTypePage.getByLabel(`${question} test`).check();
|
||||
break;
|
||||
case "multiemail":
|
||||
await eventTypePage.getByRole("button", { name: "multiemail test" }).click();
|
||||
await eventTypePage.getByPlaceholder("multiemail test").fill(EMAIL);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export async function loginUser(users: UserFixture) {
|
||||
const pro = await users.create({ name: "testuser" });
|
||||
await pro.apiLogin();
|
||||
@@ -191,17 +22,6 @@ export async function loginUserWithTeam(users: UserFixture, role: MembershipRole
|
||||
await pro.apiLogin();
|
||||
}
|
||||
|
||||
const goToNextMonthIfNoAvailabilities = async (eventTypePage: Page) => {
|
||||
try {
|
||||
if (isLastDayOfMonth()) {
|
||||
await eventTypePage.getByTestId("view_next_month").waitFor({ timeout: 6000 });
|
||||
await eventTypePage.getByTestId("view_next_month").click();
|
||||
}
|
||||
} catch (err) {
|
||||
console.info("No need to click on view next month button");
|
||||
}
|
||||
};
|
||||
|
||||
export function createBookingPageFixture(page: Page) {
|
||||
return {
|
||||
goToEventType: async (eventType: string) => {
|
||||
@@ -216,308 +36,19 @@ export function createBookingPageFixture(page: Page) {
|
||||
goToTab: async (tabName: string) => {
|
||||
await page.getByTestId(`vertical-tab-${tabName}`).click();
|
||||
},
|
||||
|
||||
goToEventTypesPage: async () => {
|
||||
await page.goto("/event-types");
|
||||
},
|
||||
|
||||
setResolution: async (width: number, height: number) => {
|
||||
await page.setViewportSize({ width: width, height: height });
|
||||
},
|
||||
addQuestion: async (
|
||||
questionType: string,
|
||||
identifier: string,
|
||||
label: string,
|
||||
isRequired: boolean,
|
||||
placeholder?: string
|
||||
) => {
|
||||
await page.getByTestId("add-field").click();
|
||||
await page.getByTestId("test-field-type").click();
|
||||
await page.getByTestId(`select-option-${questionType}`).click();
|
||||
await page.getByLabel("Identifier").dblclick();
|
||||
await page.getByLabel("Identifier").fill(identifier);
|
||||
await page.getByLabel("Label").click();
|
||||
await page.getByLabel("Label").fill(label);
|
||||
if (placeholder) {
|
||||
await page.getByLabel("Placeholder").click();
|
||||
await page.getByLabel("Placeholder").fill(placeholder);
|
||||
}
|
||||
if (!isRequired) {
|
||||
await page.getByRole("radio", { name: "No" }).click();
|
||||
}
|
||||
await page.getByTestId("field-add-save").click();
|
||||
},
|
||||
updateRecurringTab: async (repeatWeek: string, maxEvents: string) => {
|
||||
const repeatText = (await localize("en"))("repeats_every");
|
||||
const maximumOf = (await localize("en"))("for_a_maximum_of");
|
||||
await page.getByTestId("recurring-event-check").click();
|
||||
await page
|
||||
.getByTestId("recurring-event-collapsible")
|
||||
.locator("div")
|
||||
.filter({ hasText: repeatText })
|
||||
.getByRole("spinbutton")
|
||||
.fill(repeatWeek);
|
||||
await page
|
||||
.getByTestId("recurring-event-collapsible")
|
||||
.locator("div")
|
||||
.filter({ hasText: maximumOf })
|
||||
.getByRole("spinbutton")
|
||||
.fill(maxEvents);
|
||||
},
|
||||
updateEventType: async () => {
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
const toast = await page.waitForSelector('[data-testid="toast-success"]');
|
||||
expect(toast).toBeTruthy();
|
||||
},
|
||||
|
||||
previewEventType: async () => {
|
||||
const eventtypePromise = page.waitForEvent("popup");
|
||||
await page.getByTestId("preview-button").click();
|
||||
return eventtypePromise;
|
||||
},
|
||||
|
||||
selectTimeSlot: async (eventTypePage: Page) => {
|
||||
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||
await eventTypePage.getByTestId("time").first().click();
|
||||
},
|
||||
|
||||
clickReschedule: async () => {
|
||||
await page.getByText("Reschedule").click();
|
||||
},
|
||||
|
||||
selectFirstAvailableTime: async () => {
|
||||
await page.getByTestId("time").first().click();
|
||||
},
|
||||
|
||||
fillRescheduleReasonAndConfirm: async () => {
|
||||
await page.getByPlaceholder(reschedulePlaceholderText).click();
|
||||
await page.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
|
||||
await page.getByTestId("confirm-reschedule-button").click();
|
||||
},
|
||||
|
||||
fillRecurringFieldAndConfirm: async (eventTypePage: Page) => {
|
||||
await eventTypePage.getByTestId("occurrence-input").click();
|
||||
await eventTypePage.getByTestId("occurrence-input").fill("2");
|
||||
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||
await eventTypePage.getByTestId("time").first().click();
|
||||
await expect(eventTypePage.getByTestId("recurring-dates")).toBeVisible();
|
||||
},
|
||||
|
||||
cancelBookingWithReason: async (page: Page) => {
|
||||
await page.getByTestId("cancel").click();
|
||||
await page.getByTestId("cancel_reason").fill("Test cancel");
|
||||
await page.getByTestId("confirm_cancel").click();
|
||||
},
|
||||
|
||||
assertBookingCanceled: async (page: Page) => {
|
||||
await expect(page.getByTestId("cancelled-headline")).toBeVisible();
|
||||
},
|
||||
|
||||
rescheduleBooking: async (eventTypePage: Page) => {
|
||||
await goToNextMonthIfNoAvailabilities(eventTypePage);
|
||||
await eventTypePage.getByText("Reschedule").click();
|
||||
while (await eventTypePage.getByRole("button", { name: "View next" }).isVisible()) {
|
||||
await eventTypePage.getByRole("button", { name: "View next" }).click();
|
||||
}
|
||||
await eventTypePage.getByTestId("time").first().click();
|
||||
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).click();
|
||||
await eventTypePage.getByPlaceholder(reschedulePlaceholderText).fill("Test reschedule");
|
||||
await eventTypePage.getByTestId("confirm-reschedule-button").click();
|
||||
await eventTypePage.waitForTimeout(400);
|
||||
if (
|
||||
await eventTypePage.getByRole("heading", { name: "Could not reschedule the meeting." }).isVisible()
|
||||
) {
|
||||
await eventTypePage.getByTestId("back").click();
|
||||
await eventTypePage.getByTestId("time").last().click();
|
||||
await eventTypePage.getByTestId("confirm-reschedule-button").click();
|
||||
}
|
||||
},
|
||||
|
||||
assertBookingRescheduled: async (page: Page) => {
|
||||
await expect(page.getByText(scheduleSuccessfullyText)).toBeVisible();
|
||||
},
|
||||
|
||||
assertRepeatEventType: async () => {
|
||||
await expect(page.getByTestId("repeat-eventtype")).toBeVisible();
|
||||
},
|
||||
|
||||
cancelBooking: async (eventTypePage: Page) => {
|
||||
await eventTypePage.getByTestId("cancel").click();
|
||||
await eventTypePage.getByTestId("cancel_reason").fill("Test cancel");
|
||||
await eventTypePage.getByTestId("confirm_cancel").click();
|
||||
await expect(eventTypePage.getByTestId("cancelled-headline")).toBeVisible();
|
||||
},
|
||||
|
||||
confirmBooking: async (eventTypePage: Page) => {
|
||||
await eventTypePage.getByTestId("confirm-book-button").click();
|
||||
await eventTypePage.waitForURL("booking/**");
|
||||
await expect(eventTypePage.getByText(scheduleSuccessfullyText)).toBeVisible();
|
||||
},
|
||||
|
||||
fillAndConfirmBooking: async ({
|
||||
eventTypePage,
|
||||
placeholderText,
|
||||
question,
|
||||
fillText,
|
||||
secondQuestion,
|
||||
options,
|
||||
}: fillAndConfirmBookingParams) => {
|
||||
const confirmButton = options.isReschedule ? "confirm-reschedule-button" : "confirm-book-button";
|
||||
|
||||
await expect(eventTypePage.getByText(`${secondQuestion} test`).first()).toBeVisible();
|
||||
await eventTypePage.getByPlaceholder(placeholderText).fill(fillText);
|
||||
|
||||
// Change the selector for specifics cases related to select question
|
||||
const shouldChangeSelectLocator = (question: string, secondQuestion: string): boolean =>
|
||||
question === "select" && ["multiemail", "multiselect", "address"].includes(secondQuestion);
|
||||
|
||||
const shouldUseLastRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
|
||||
question === "radio" && secondQuestion === "checkbox";
|
||||
|
||||
const shouldUseFirstRadioGroupLocator = (question: string, secondQuestion: string): boolean =>
|
||||
question === "checkbox" && secondQuestion === "radio";
|
||||
|
||||
const shouldChangeMultiSelectLocator = (question: string, secondQuestion: string): boolean =>
|
||||
question === "multiselect" &&
|
||||
["address", "checkbox", "multiemail", "select"].includes(secondQuestion);
|
||||
|
||||
const customLocators = {
|
||||
shouldChangeSelectLocator: shouldChangeSelectLocator(question, secondQuestion),
|
||||
shouldUseLastRadioGroupLocator: shouldUseLastRadioGroupLocator(question, secondQuestion),
|
||||
shouldUseFirstRadioGroupLocator: shouldUseFirstRadioGroupLocator(question, secondQuestion),
|
||||
shouldChangeMultiSelectLocator: shouldChangeMultiSelectLocator(question, secondQuestion),
|
||||
};
|
||||
|
||||
// Fill the first question
|
||||
await fillQuestion(eventTypePage, question, customLocators);
|
||||
|
||||
// Fill the second question if is required
|
||||
options.isRequired && (await fillQuestion(eventTypePage, secondQuestion, customLocators));
|
||||
|
||||
await eventTypePage.getByTestId(confirmButton).click();
|
||||
await eventTypePage.waitForTimeout(400);
|
||||
if (await eventTypePage.getByRole("heading", { name: "Could not book the meeting." }).isVisible()) {
|
||||
await eventTypePage.getByTestId("back").click();
|
||||
await eventTypePage.getByTestId("time").last().click();
|
||||
await fillQuestion(eventTypePage, question, customLocators);
|
||||
options.isRequired && (await fillQuestion(eventTypePage, secondQuestion, customLocators));
|
||||
await eventTypePage.getByTestId(confirmButton).click();
|
||||
}
|
||||
const scheduleSuccessfullyPage = eventTypePage.getByText(scheduleSuccessfullyText);
|
||||
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
|
||||
await expect(scheduleSuccessfullyPage).toBeVisible();
|
||||
},
|
||||
|
||||
fillAllQuestions: async (eventTypePage: Page, questions: string[], options: BookingOptions) => {
|
||||
const confirmButton = options.isReschedule ? "confirm-reschedule-button" : "confirm-book-button";
|
||||
await fillAllQuestions(eventTypePage, questions, options);
|
||||
await eventTypePage.getByTestId(confirmButton).click();
|
||||
const scheduleSuccessfullyPage = eventTypePage.getByText(scheduleSuccessfullyText);
|
||||
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
|
||||
await expect(scheduleSuccessfullyPage).toBeVisible();
|
||||
},
|
||||
addGuests: async (eventTypePage: Page, options: { guests: string[] }) => {
|
||||
await eventTypePage.getByTestId("add-guests").click();
|
||||
for (const guest of options.guests) {
|
||||
await eventTypePage.getByPlaceholder("Email").fill(guest);
|
||||
}
|
||||
},
|
||||
createTeam: async (name: string) => {
|
||||
await page.getByTestId("teams-test").click();
|
||||
await page.getByTestId("new-team-btn").click();
|
||||
await page.getByPlaceholder("Acme Inc.").click();
|
||||
await page.getByPlaceholder("Acme Inc.").fill(name);
|
||||
await page.getByTestId("continue-button").click();
|
||||
await page.getByTestId("publish-button").click();
|
||||
|
||||
await page.getByTestId("vertical-tab-Back").click();
|
||||
},
|
||||
|
||||
createTeamEventType: async (name: string, options: teamBookingtypes) => {
|
||||
await page.getByTestId("new-event-type").click();
|
||||
await page.getByTestId("option-0").click();
|
||||
|
||||
// We first simulate to create a default event type to check if managed option is not available
|
||||
|
||||
const managedEventDescription = (await localize("en"))("managed_event_description");
|
||||
const roundRobinEventDescription = (await localize("en"))("round_robin_description");
|
||||
const collectiveEventDescription = (await localize("en"))("collective_description");
|
||||
const quickChatText = (await localize("en"))("quick_chat");
|
||||
await expect(page.locator("div").filter({ hasText: managedEventDescription })).toBeHidden();
|
||||
await page.getByTestId("dialog-rejection").click();
|
||||
|
||||
await page.getByTestId("new-event-type").click();
|
||||
await page.getByTestId("option-team-1").click();
|
||||
await page.getByPlaceholder(quickChatText).fill(name);
|
||||
if (options.isCollectiveType) {
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: `Collective${collectiveEventDescription}` })
|
||||
.getByRole("radio")
|
||||
.first()
|
||||
.click();
|
||||
}
|
||||
|
||||
if (options.isRoundRobinType) {
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: `Round Robin${roundRobinEventDescription}` })
|
||||
.getByRole("radio")
|
||||
.nth(1)
|
||||
.click();
|
||||
}
|
||||
|
||||
if (options.isManagedType) {
|
||||
await page
|
||||
.locator("div")
|
||||
.filter({ hasText: `Managed Event${managedEventDescription}` })
|
||||
.getByRole("radio")
|
||||
.last()
|
||||
.click();
|
||||
|
||||
const managedEventClarification = (await localize("en"))("managed_event_url_clarification");
|
||||
await expect(page.getByText(managedEventClarification)).toBeVisible();
|
||||
}
|
||||
|
||||
const continueText = (await localize("en"))("continue");
|
||||
const toast = page.getByTestId("toast-success");
|
||||
|
||||
await page.getByRole("button", { name: continueText }).click();
|
||||
expect(toast).toBeTruthy();
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
},
|
||||
assertLabelWithCorrectTeamName: async (page: Page, teamName: string) => {
|
||||
await expect(page.getByRole("link", { name: teamName }).first()).toBeVisible();
|
||||
},
|
||||
assertBookingWithCorrectTitleAndDescription: async (
|
||||
page: Page,
|
||||
options: { profileName: string; bookingName: string; teamName: string; aditionalGuestEmail?: string }
|
||||
) => {
|
||||
options.aditionalGuestEmail
|
||||
? await expect(
|
||||
page.getByRole("link", {
|
||||
name: `${options.bookingName} between ${options.teamName} and ${options.profileName} You , ${options.profileName} and ${options.aditionalGuestEmail}`,
|
||||
})
|
||||
).toBeVisible()
|
||||
: await expect(
|
||||
page.getByRole("link", {
|
||||
name: `${options.bookingName} between ${options.teamName} and ${options.profileName} You and ${options.profileName}`,
|
||||
})
|
||||
).toBeVisible();
|
||||
},
|
||||
clickOnBooking: async (page: Page, teamName: string) => {
|
||||
await page.getByRole("link", { name: teamName }).first().click();
|
||||
},
|
||||
|
||||
checkField: async (question: string, options?: { isOptional: boolean }) => {
|
||||
if (options?.isOptional) {
|
||||
await expect(page.getByTestId(`field-${question}-test`).getByTestId("optional")).toBeVisible();
|
||||
} else {
|
||||
await expect(page.getByTestId(`field-${question}-test`).getByTestId("required")).toBeVisible();
|
||||
}
|
||||
await expect(page.getByTestId(`field-${question}-test`)).toBeVisible();
|
||||
},
|
||||
|
||||
checkRequiresConfirmation: async () => {
|
||||
// Check existence of the icon
|
||||
await expect(page.getByTestId("requires-confirmation-title").locator("svg")).toBeVisible();
|
||||
@@ -526,7 +57,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await expect(confirmationSwitch).toBeVisible();
|
||||
await confirmationSwitch.click();
|
||||
},
|
||||
|
||||
checkRequiresBookerEmailVerification: async () => {
|
||||
await expect(page.getByTestId("requires-booker-email-verification-title").locator("svg")).toBeVisible();
|
||||
|
||||
@@ -535,7 +65,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await expect(emailSwitch).toBeVisible();
|
||||
await emailSwitch.click();
|
||||
},
|
||||
|
||||
checkHideNotes: async () => {
|
||||
await expect(page.getByTestId("disable-notes-title").locator("svg")).toBeVisible();
|
||||
|
||||
@@ -544,7 +73,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await expect(hideNotesSwitch).toBeVisible();
|
||||
await hideNotesSwitch.click();
|
||||
},
|
||||
|
||||
checkRedirectOnBooking: async () => {
|
||||
await expect(page.getByTestId("redirect-success-booking-title").locator("svg")).toBeVisible();
|
||||
|
||||
@@ -555,7 +83,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await page.getByTestId("external-redirect-url").fill("https://cal.com");
|
||||
await expect(page.getByTestId("redirect-url-warning")).toBeVisible();
|
||||
},
|
||||
|
||||
checkEnablePrivateUrl: async () => {
|
||||
await expect(page.getByTestId("hashedLinkCheck-title").locator("label div")).toBeVisible();
|
||||
|
||||
@@ -564,7 +91,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await page.getByTestId("hashedLinkCheck").click();
|
||||
await expect(page.getByTestId("generated-hash-url")).toBeVisible();
|
||||
},
|
||||
|
||||
toggleOfferSeats: async () => {
|
||||
await expect(page.getByTestId("offer-seats-toggle-title").locator("svg")).toBeVisible();
|
||||
|
||||
@@ -575,7 +101,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await expect(seatSwitchField).toHaveValue("3");
|
||||
await expect(page.getByTestId("show-attendees")).toBeVisible();
|
||||
},
|
||||
|
||||
checkLockTimezone: async () => {
|
||||
await expect(page.getByTestId("lock-timezone-toggle-title").locator("svg")).toBeVisible();
|
||||
|
||||
@@ -584,18 +109,9 @@ export function createBookingPageFixture(page: Page) {
|
||||
await expect(lockSwitch).toBeVisible();
|
||||
await lockSwitch.click();
|
||||
},
|
||||
|
||||
checkEventType: async () => {
|
||||
await expect(page.getByTestId("requires-confirmation-badge").last()).toBeVisible();
|
||||
},
|
||||
|
||||
assertBookingIsVisible: async (name: string) => {
|
||||
await expect(
|
||||
page.getByRole("link", {
|
||||
name,
|
||||
})
|
||||
).toBeVisible();
|
||||
},
|
||||
checkBufferTime: async () => {
|
||||
const minutes = (await localize("en"))("minutes");
|
||||
const fieldPlaceholder = page.getByPlaceholder("0");
|
||||
@@ -629,7 +145,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await page.getByTestId("select-option-60").click();
|
||||
await expect(page.getByText(`60 ${minutes}`, { exact: true })).toBeVisible();
|
||||
},
|
||||
|
||||
checkLimitBookingFrequency: async () => {
|
||||
const fieldPlaceholder = page.getByPlaceholder("1").nth(1);
|
||||
const limitFrequency = (await localize("en"))("limit_booking_frequency");
|
||||
@@ -645,7 +160,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await expect(fieldPlaceholder).toHaveValue("12");
|
||||
await limitFrequencySwitch.click();
|
||||
},
|
||||
|
||||
checkLimitBookingDuration: async () => {
|
||||
const limitDuration = (await localize("en"))("limit_total_booking_duration");
|
||||
const addlimit = (await localize("en"))("add_limit");
|
||||
@@ -659,7 +173,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await expect(page.getByTestId("add-limit")).toHaveCount(2);
|
||||
await limitDurationSwitch.click();
|
||||
},
|
||||
|
||||
checkLimitFutureBookings: async () => {
|
||||
const limitFutureBookings = (await localize("en"))("limit_future_bookings");
|
||||
const limitBookingsSwitch = page
|
||||
@@ -672,7 +185,6 @@ export function createBookingPageFixture(page: Page) {
|
||||
await expect(page.locator("#RANGE")).toBeChecked();
|
||||
await limitBookingsSwitch.click();
|
||||
},
|
||||
|
||||
checkOffsetTimes: async () => {
|
||||
const offsetStart = (await localize("en"))("offset_start");
|
||||
const offsetStartTimes = (await localize("en"))("offset_toggle");
|
||||
@@ -685,54 +197,8 @@ export function createBookingPageFixture(page: Page) {
|
||||
page.getByText("e.g. this will show time slots to your bookers at 9:10 AM instead of 9:00 AM")
|
||||
).toBeVisible();
|
||||
},
|
||||
|
||||
checkTimeSlotsCount: async (eventTypePage: Page, count: number) => {
|
||||
await expect(eventTypePage.getByTestId("time")).toHaveCount(count);
|
||||
},
|
||||
createBookingWebhook: async (webhookReceiver: { url: string }, eventTitle: string) => {
|
||||
const events = [
|
||||
"Booking Canceled",
|
||||
"Booking Created",
|
||||
"Booking Rejected",
|
||||
"Booking Requested",
|
||||
"Booking Payment Initiated",
|
||||
"Booking Rescheduled",
|
||||
"Meeting Ended",
|
||||
"Booking Paid",
|
||||
"Recording Download Link Ready",
|
||||
];
|
||||
await page.goto(`${WEBAPP_URL}/event-types`);
|
||||
await page.getByText(eventTitle).click();
|
||||
|
||||
await page.getByTestId("vertical-tab-webhooks").click();
|
||||
|
||||
await page.getByTestId("new_webhook").click();
|
||||
await page.getByLabel("Subscriber URL").fill(webhookReceiver.url);
|
||||
await page.getByRole("button", { name: "Ping test" }).click();
|
||||
page
|
||||
.getByTestId("dialog-creation")
|
||||
.locator("div")
|
||||
.filter({ hasText: '{ "ok": true, "status": 200, "message": "{}" }' })
|
||||
.nth(2);
|
||||
await page.getByRole("button", { name: "Create Webhook" }).click();
|
||||
|
||||
page.getByText(webhookReceiver.url);
|
||||
events.forEach((event) => {
|
||||
page.getByText(event);
|
||||
});
|
||||
},
|
||||
rejectFirstBooking: async () => {
|
||||
await page.goto("/bookings/unconfirmed");
|
||||
await page.getByTestId("horizontal-tab-unconfirmed").click();
|
||||
await page.getByTestId("booking-item").first().getByText("Reject").click();
|
||||
await page.getByTestId("rejection-confirm").click();
|
||||
},
|
||||
fillEmailAndName: async (eventTypePage: Page) => {
|
||||
const emailLabel = (await localize("en"))("email_address");
|
||||
const nameLabel = (await localize("en"))("your_name");
|
||||
|
||||
await eventTypePage.getByLabel(emailLabel).fill(EMAIL);
|
||||
await eventTypePage.getByLabel(nameLabel).fill("testuser");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -120,18 +120,21 @@ const createTeamAndAddUser = async (
|
||||
const slug = `${isOrg ? "org" : "team"}-${workerInfo.workerIndex}-${Date.now()}`;
|
||||
const data: PrismaType.TeamCreateInput = {
|
||||
name: `user-id-${user.id}'s ${isOrg ? "Org" : "Team"}`,
|
||||
isOrganization: isOrg,
|
||||
};
|
||||
data.metadata = {
|
||||
...(isUnpublished ? { requestedSlug: slug } : {}),
|
||||
...(isOrg
|
||||
? {
|
||||
isOrganization: true,
|
||||
isOrganizationVerified: !!isOrgVerified,
|
||||
orgAutoAcceptEmail: user.email.split("@")[1],
|
||||
isOrganizationConfigured: false,
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
if (isOrg) {
|
||||
data.organizationSettings = {
|
||||
create: {
|
||||
isOrganizationVerified: !!isOrgVerified,
|
||||
orgAutoAcceptEmail: user.email.split("@")[1],
|
||||
isOrganizationConfigured: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
data.slug = !isUnpublished ? slug : undefined;
|
||||
if (isOrg && hasSubteam) {
|
||||
const team = await createTeamAndAddUser({ user }, workerInfo);
|
||||
@@ -518,6 +521,7 @@ export const createUsersFixture = (
|
||||
// Delete all users that were tracked by email(if they were created)
|
||||
await prisma.user.deleteMany({ where: { email: { in: store.trackedEmails.map((e) => e.email) } } });
|
||||
await prisma.team.deleteMany({ where: { id: { in: store.teams.map((org) => org.id) } } });
|
||||
await prisma.secondaryEmail.deleteMany({ where: { userId: { in: ids } } });
|
||||
store.users = [];
|
||||
store.teams = [];
|
||||
store.trackedEmails = [];
|
||||
@@ -593,25 +597,40 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||
},
|
||||
};
|
||||
})
|
||||
.find((membership) => !membership.team?.metadata?.isOrganization);
|
||||
.find((membership) => !membership.team.isOrganization);
|
||||
if (!membership) {
|
||||
throw new Error("No team found for user");
|
||||
}
|
||||
return membership;
|
||||
},
|
||||
getOrgMembership: async () => {
|
||||
return prisma.membership.findFirstOrThrow({
|
||||
const membership = await prisma.membership.findFirstOrThrow({
|
||||
where: {
|
||||
userId: user.id,
|
||||
team: {
|
||||
metadata: {
|
||||
path: ["isOrganization"],
|
||||
equals: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
team: {
|
||||
include: {
|
||||
children: true,
|
||||
organizationSettings: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
include: { team: { include: { children: true } } },
|
||||
});
|
||||
if (!membership) {
|
||||
return membership;
|
||||
}
|
||||
|
||||
return {
|
||||
...membership,
|
||||
team: {
|
||||
...membership.team,
|
||||
metadata: teamMetadataSchema.parse(membership.team.metadata),
|
||||
},
|
||||
};
|
||||
},
|
||||
getFirstEventAsOwner: async () =>
|
||||
prisma.eventType.findFirstOrThrow({
|
||||
@@ -652,7 +671,6 @@ type SupportedTestWorkflows = PrismaType.WorkflowCreateInput;
|
||||
|
||||
type CustomUserOptsKeys =
|
||||
| "username"
|
||||
| "password"
|
||||
| "completedOnboarding"
|
||||
| "locale"
|
||||
| "name"
|
||||
@@ -669,6 +687,8 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||
useExactUsername?: boolean;
|
||||
roleInOrganization?: MembershipRole;
|
||||
schedule?: Schedule;
|
||||
password?: string | null;
|
||||
emailDomain?: string;
|
||||
};
|
||||
|
||||
// creates the actual user in the db.
|
||||
@@ -686,11 +706,16 @@ const createUser = (
|
||||
? opts.username
|
||||
: `${opts?.username || "user"}-${workerInfo.workerIndex}-${Date.now()}`;
|
||||
|
||||
const emailDomain = opts?.emailDomain || "example.com";
|
||||
return {
|
||||
username: uname,
|
||||
name: opts?.name,
|
||||
email: opts?.email ?? `${uname}@example.com`,
|
||||
password: hashPassword(uname),
|
||||
email: opts?.email ?? `${uname}@${emailDomain}`,
|
||||
password: {
|
||||
create: {
|
||||
hash: hashPassword(uname),
|
||||
},
|
||||
},
|
||||
emailVerified: new Date(),
|
||||
completedOnboarding: opts?.completedOnboarding ?? true,
|
||||
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
||||
@@ -791,7 +816,7 @@ async function confirmPendingPayment(page: Page) {
|
||||
|
||||
// login using a replay of an E2E routine.
|
||||
export async function login(
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "email">> & { password?: string | null },
|
||||
page: Page
|
||||
) {
|
||||
// get locators
|
||||
@@ -812,7 +837,7 @@ export async function login(
|
||||
}
|
||||
|
||||
export async function apiLogin(
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "password" | "email">>,
|
||||
user: Pick<Prisma.User, "username"> & Partial<Pick<Prisma.User, "email">> & { password: string | null },
|
||||
page: Page
|
||||
) {
|
||||
const csrfToken = await page
|
||||
|
||||
@@ -4,6 +4,7 @@ import { test as base } from "@playwright/test";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { ExpectedUrlDetails } from "../../../../playwright.config";
|
||||
import { createAppsFixture } from "../fixtures/apps";
|
||||
import { createBookingsFixture } from "../fixtures/bookings";
|
||||
import { createEmailsFixture } from "../fixtures/emails";
|
||||
import { createEmbedsFixture } from "../fixtures/embeds";
|
||||
@@ -32,6 +33,7 @@ export interface Fixtures {
|
||||
workflowPage: ReturnType<typeof createWorkflowPageFixture>;
|
||||
features: ReturnType<typeof createFeatureFixture>;
|
||||
eventTypePage: ReturnType<typeof createEventTypeFixture>;
|
||||
appsPage: ReturnType<typeof createAppsFixture>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
@@ -104,4 +106,8 @@ export const test = base.extend<Fixtures>({
|
||||
const eventTypePage = createEventTypeFixture(page);
|
||||
await use(eventTypePage);
|
||||
},
|
||||
appsPage: async ({ page }, use) => {
|
||||
const appsPage = createAppsFixture(page);
|
||||
await use(appsPage);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -351,7 +351,7 @@ test.describe("Bookings", () => {
|
||||
}
|
||||
const usernameInOrg = getOrgUsernameFromEmail(
|
||||
invitedUserEmail,
|
||||
org.metadata?.orgAutoAcceptEmail ?? null
|
||||
org.organizationSettings?.orgAutoAcceptEmail ?? null
|
||||
);
|
||||
|
||||
const usernameOutsideOrg = userOutsideOrganization.username;
|
||||
|
||||
@@ -2,6 +2,9 @@ import type { Browser, Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/client";
|
||||
|
||||
import { moveUserToOrg } from "@lib/orgMigration";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { getInviteLink } from "../lib/testUtils";
|
||||
@@ -16,7 +19,7 @@ test.afterEach(async ({ users, orgs }) => {
|
||||
|
||||
test.describe("Organization", () => {
|
||||
test.describe("Email not matching orgAutoAcceptEmail", () => {
|
||||
test("Org Invitation", async ({ browser, page, users, emails }) => {
|
||||
test("nonexisting user invited to an organization", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
@@ -83,7 +86,17 @@ test.describe("Organization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Team invitation", async ({ browser, page, users, emails }) => {
|
||||
// This test is already covered by booking.e2e.ts where existing user is invited and his booking links are tested.
|
||||
// We can re-test here when we want to test some more scenarios.
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
||||
test("existing user invited to an organization", () => {});
|
||||
|
||||
test("nonexisting user invited to a Team inside organization", async ({
|
||||
browser,
|
||||
page,
|
||||
users,
|
||||
emails,
|
||||
}) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, hasSubteam: true });
|
||||
await orgOwner.apiLogin();
|
||||
const { team } = await orgOwner.getFirstTeamMembership();
|
||||
@@ -183,7 +196,7 @@ test.describe("Organization", () => {
|
||||
});
|
||||
|
||||
test.describe("Email matching orgAutoAcceptEmail and a Verified Organization", () => {
|
||||
test("Org Invitation", async ({ browser, page, users, emails }) => {
|
||||
test("nonexisting user is invited to Org", async ({ browser, page, users, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
@@ -248,7 +261,52 @@ test.describe("Organization", () => {
|
||||
});
|
||||
});
|
||||
|
||||
test("Team Invitation", async ({ browser, page, users, emails }) => {
|
||||
// Such a user has user.username changed directly in addition to having the new username in the profile.username
|
||||
test("existing user migrated to an organization", async ({ users, page, emails }) => {
|
||||
const orgOwner = await users.create(undefined, { hasTeam: true, isOrg: true, isOrgVerified: true });
|
||||
const { team: org } = await orgOwner.getOrgMembership();
|
||||
await orgOwner.apiLogin();
|
||||
const { existingUser } = await test.step("Invite an existing user to an organization", async () => {
|
||||
const existingUser = await users.create({
|
||||
username: "john",
|
||||
emailDomain: org.organizationSettings?.orgAutoAcceptEmail ?? "",
|
||||
name: "John Outside Organization",
|
||||
});
|
||||
|
||||
await moveUserToOrg({
|
||||
user: existingUser,
|
||||
targetOrg: {
|
||||
username: `${existingUser.username}-org`,
|
||||
id: org.id,
|
||||
membership: {
|
||||
role: MembershipRole.MEMBER,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
shouldMoveTeams: false,
|
||||
});
|
||||
return { existingUser };
|
||||
});
|
||||
|
||||
await test.step("Signing up with the previous username of the migrated user - shouldn't be allowed", async () => {
|
||||
await page.goto("/signup");
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
await page.locator('input[name="username"]').fill(existingUser.username!);
|
||||
await page
|
||||
.locator('input[name="email"]')
|
||||
.fill(`${existingUser.username}-differnet-email@example.com`);
|
||||
await page.locator('input[name="password"]').fill("Password99!");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator('button[type="submit"]')).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
test("nonexisting user is invited to a team inside organization", async ({
|
||||
browser,
|
||||
page,
|
||||
users,
|
||||
emails,
|
||||
}) => {
|
||||
const orgOwner = await users.create(undefined, {
|
||||
hasTeam: true,
|
||||
isOrg: true,
|
||||
|
||||
@@ -1,8 +1,14 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import type { Page } from "@playwright/test";
|
||||
import type { createUsersFixture } from "playwright/fixtures/users";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import type { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
import type { createEmailsFixture } from "./fixtures/emails";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { getEmailsReceivedByUser } from "./lib/testUtils";
|
||||
import { expectInvitationEmailToBeReceived } from "./team/expects";
|
||||
|
||||
test.describe.configure({ mode: "parallel" });
|
||||
|
||||
@@ -48,7 +54,7 @@ test.describe("Update Profile", () => {
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/profile");
|
||||
|
||||
const emailInput = page.getByTestId("profile-form-email");
|
||||
const emailInput = page.getByTestId("profile-form-email-0");
|
||||
|
||||
await emailInput.fill(email);
|
||||
|
||||
@@ -58,7 +64,7 @@ test.describe("Update Profile", () => {
|
||||
|
||||
await page.getByTestId("profile-update-email-submit-button").click();
|
||||
|
||||
const toastLocator = await page.waitForSelector('[data-testId="toast-success"]');
|
||||
const toastLocator = await page.getByTestId("toast-success");
|
||||
|
||||
const textContent = await toastLocator.textContent();
|
||||
|
||||
@@ -84,12 +90,12 @@ test.describe("Update Profile", () => {
|
||||
|
||||
await page.goto(verifyUrl);
|
||||
|
||||
const errorLocator = await page.waitForSelector('[data-testId="toast-error"]');
|
||||
const errorLocator = await page.getByTestId("toast-error");
|
||||
|
||||
expect(errorLocator).toBeDefined();
|
||||
|
||||
await page.goto("/settings/my-account/profile");
|
||||
const emailInputUpdated = page.getByTestId("profile-form-email");
|
||||
const emailInputUpdated = page.getByTestId("profile-form-email-0");
|
||||
expect(await emailInputUpdated.inputValue()).toEqual(user.email);
|
||||
});
|
||||
|
||||
@@ -108,7 +114,7 @@ test.describe("Update Profile", () => {
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/profile");
|
||||
|
||||
const emailInput = page.getByTestId("profile-form-email");
|
||||
const emailInput = page.getByTestId("profile-form-email-0");
|
||||
|
||||
await emailInput.fill(email);
|
||||
|
||||
@@ -118,11 +124,7 @@ test.describe("Update Profile", () => {
|
||||
|
||||
await page.getByTestId("profile-update-email-submit-button").click();
|
||||
|
||||
const toastLocator = await page.waitForSelector('[data-testId="toast-success"]');
|
||||
|
||||
const textContent = await toastLocator.textContent();
|
||||
|
||||
expect(textContent).toContain(email);
|
||||
expect(await page.getByTestId("toast-success").textContent()).toContain(email);
|
||||
|
||||
// Instead of dealing with emails in e2e lets just get the token and navigate to it
|
||||
const verificationToken = await prisma.verificationToken.findFirst({
|
||||
@@ -140,13 +142,16 @@ test.describe("Update Profile", () => {
|
||||
|
||||
await page.goto(verifyUrl);
|
||||
|
||||
const successLocator = await page.waitForSelector('[data-testId="toast-success"]');
|
||||
expect(await page.getByTestId("toast-success").textContent()).toContain(email);
|
||||
|
||||
// After email verification is successfull. user is sent to /event-types
|
||||
await page.waitForURL("/event-types");
|
||||
|
||||
expect(await successLocator.textContent()).toContain(email);
|
||||
await page.goto("/settings/my-account/profile");
|
||||
const emailInputUpdated = page.getByTestId("profile-form-email");
|
||||
const emailInputUpdated = await page.getByTestId("profile-form-email-0");
|
||||
expect(await emailInputUpdated.inputValue()).toEqual(email);
|
||||
});
|
||||
|
||||
test("Can update a users email (verification disabled)", async ({ page, users, prisma, features }) => {
|
||||
const emailVerificationEnabled = features.get("email-verification");
|
||||
// eslint-disable-next-line playwright/no-conditional-in-test, playwright/no-skipped-test
|
||||
@@ -162,7 +167,7 @@ test.describe("Update Profile", () => {
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/profile");
|
||||
|
||||
const emailInput = page.getByTestId("profile-form-email");
|
||||
const emailInput = page.getByTestId("profile-form-email-0");
|
||||
|
||||
await emailInput.fill(email);
|
||||
|
||||
@@ -172,12 +177,235 @@ test.describe("Update Profile", () => {
|
||||
|
||||
await page.getByTestId("profile-update-email-submit-button").click();
|
||||
|
||||
const toastLocator = await page.waitForSelector('[data-testId="toast-success"]');
|
||||
expect(await page.getByTestId("toast-success").isVisible()).toBe(true);
|
||||
|
||||
expect(await toastLocator.isVisible()).toBe(true);
|
||||
|
||||
const emailInputUpdated = page.getByTestId("profile-form-email");
|
||||
const emailInputUpdated = page.getByTestId("profile-form-email-0");
|
||||
|
||||
expect(await emailInputUpdated.inputValue()).toEqual(email);
|
||||
});
|
||||
|
||||
const testEmailVerificationLink = async ({
|
||||
page,
|
||||
prisma,
|
||||
emails,
|
||||
secondaryEmail,
|
||||
}: {
|
||||
page: Page;
|
||||
prisma: PrismaClient;
|
||||
emails: ReturnType<typeof createEmailsFixture>;
|
||||
secondaryEmail: string;
|
||||
}) => {
|
||||
await test.step("the user receives the correct invitation link", async () => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
const verificationToken = await prisma.verificationToken.findFirst({
|
||||
where: {
|
||||
identifier: secondaryEmail,
|
||||
},
|
||||
});
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
secondaryEmail,
|
||||
"Verify your email address",
|
||||
"verify-email"
|
||||
);
|
||||
expect(inviteLink).toEqual(`${WEBAPP_URL}/api/auth/verify-email?token=${verificationToken?.token}`);
|
||||
});
|
||||
};
|
||||
|
||||
test("Can add a new email as a secondary email", async ({ page, users, prisma, emails }) => {
|
||||
const user = await users.create({
|
||||
name: "update-profile-user",
|
||||
});
|
||||
|
||||
const [emailInfo, emailDomain] = user.email.split("@");
|
||||
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/profile");
|
||||
|
||||
await page.getByTestId("add-secondary-email").click();
|
||||
|
||||
const secondaryEmailAddDialog = await page.waitForSelector('[data-testid="secondary-email-add-dialog"]');
|
||||
expect(await secondaryEmailAddDialog.isVisible()).toBe(true);
|
||||
|
||||
const secondaryEmail = `${emailInfo}-secondary-email@${emailDomain}`;
|
||||
const secondaryEmailInput = page.getByTestId("secondary-email-input");
|
||||
await secondaryEmailInput.fill(secondaryEmail);
|
||||
|
||||
await page.getByTestId("add-secondary-email-button").click();
|
||||
|
||||
const secondaryEmailConfirmDialog = await page.waitForSelector(
|
||||
'[data-testid="secondary-email-confirm-dialog"]'
|
||||
);
|
||||
expect(await secondaryEmailConfirmDialog.isVisible()).toBe(true);
|
||||
|
||||
const textContent = await secondaryEmailConfirmDialog.textContent();
|
||||
expect(textContent).toContain(secondaryEmail);
|
||||
|
||||
await page.getByTestId("secondary-email-confirm-done-button").click();
|
||||
expect(await secondaryEmailConfirmDialog.isVisible()).toBe(false);
|
||||
|
||||
await test.step("the user receives the correct invitation link", async () => {
|
||||
await page.waitForLoadState("networkidle");
|
||||
const verificationToken = await prisma.verificationToken.findFirst({
|
||||
where: {
|
||||
identifier: secondaryEmail,
|
||||
},
|
||||
});
|
||||
const inviteLink = await expectInvitationEmailToBeReceived(
|
||||
page,
|
||||
emails,
|
||||
secondaryEmail,
|
||||
"Verify your email address",
|
||||
"verify-email"
|
||||
);
|
||||
expect(inviteLink?.endsWith(`/api/auth/verify-email?token=${verificationToken?.token}`)).toEqual(true);
|
||||
});
|
||||
|
||||
const primaryEmail = page.getByTestId("profile-form-email-0");
|
||||
expect(await primaryEmail.inputValue()).toEqual(user.email);
|
||||
|
||||
const newlyAddedSecondaryEmail = page.getByTestId("profile-form-email-1");
|
||||
expect(await newlyAddedSecondaryEmail.inputValue()).toEqual(secondaryEmail);
|
||||
});
|
||||
|
||||
const createSecondaryEmail = async ({
|
||||
page,
|
||||
users,
|
||||
}: {
|
||||
page: Page;
|
||||
users: ReturnType<typeof createUsersFixture>;
|
||||
}) => {
|
||||
const user = await users.create({
|
||||
name: "update-profile-user",
|
||||
});
|
||||
|
||||
const [emailInfo, emailDomain] = user.email.split("@");
|
||||
const email = `${emailInfo}@${emailDomain}`;
|
||||
|
||||
await user.apiLogin();
|
||||
await page.goto("/settings/my-account/profile");
|
||||
|
||||
await page.getByTestId("add-secondary-email").click();
|
||||
|
||||
const secondaryEmail = `${emailInfo}-secondary-email@${emailDomain}`;
|
||||
const secondaryEmailInput = await page.getByTestId("secondary-email-input");
|
||||
await secondaryEmailInput.fill(secondaryEmail);
|
||||
|
||||
await page.getByTestId("add-secondary-email-button").click();
|
||||
|
||||
await page.getByTestId("secondary-email-confirm-done-button").click();
|
||||
|
||||
return { user, email, secondaryEmail };
|
||||
};
|
||||
|
||||
test("Newly added secondary email should show as Unverified", async ({ page, users }) => {
|
||||
await createSecondaryEmail({ page, users });
|
||||
|
||||
expect(await page.getByTestId("profile-form-email-0-primary-badge").isVisible()).toEqual(true);
|
||||
expect(await page.getByTestId("profile-form-email-0-unverified-badge").isVisible()).toEqual(false);
|
||||
|
||||
expect(await page.getByTestId("profile-form-email-1-primary-badge").isVisible()).toEqual(false);
|
||||
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(true);
|
||||
});
|
||||
|
||||
test("Can verify the newly added secondary email", async ({ page, users, prisma }) => {
|
||||
const { secondaryEmail } = await createSecondaryEmail({ page, users });
|
||||
|
||||
expect(await page.getByTestId("profile-form-email-1-primary-badge").isVisible()).toEqual(false);
|
||||
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(true);
|
||||
// Instead of dealing with emails in e2e lets just get the token and navigate to it
|
||||
const verificationToken = await prisma.verificationToken.findFirst({
|
||||
where: {
|
||||
identifier: secondaryEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const params = new URLSearchParams({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
token: verificationToken!.token,
|
||||
});
|
||||
|
||||
const verifyUrl = `${WEBAPP_URL}/api/auth/verify-email?${params.toString()}`;
|
||||
|
||||
await page.goto(verifyUrl);
|
||||
|
||||
expect(await page.getByTestId("profile-form-email-1-primary-badge").isVisible()).toEqual(false);
|
||||
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(false);
|
||||
});
|
||||
|
||||
test("Can delete the newly added secondary email", async ({ page, users }) => {
|
||||
await createSecondaryEmail({ page, users });
|
||||
|
||||
await page.getByTestId("secondary-email-action-group-button").nth(1).click();
|
||||
await page.getByTestId("secondary-email-delete-button").click();
|
||||
|
||||
expect(await page.getByTestId("profile-form-email-1").isVisible()).toEqual(false);
|
||||
});
|
||||
|
||||
test("Can make the newly added secondary email as the primary email and login", async ({
|
||||
page,
|
||||
users,
|
||||
prisma,
|
||||
}) => {
|
||||
const { secondaryEmail } = await createSecondaryEmail({ page, users });
|
||||
|
||||
// Instead of dealing with emails in e2e lets just get the token and navigate to it
|
||||
const verificationToken = await prisma.verificationToken.findFirst({
|
||||
where: {
|
||||
identifier: secondaryEmail,
|
||||
},
|
||||
});
|
||||
|
||||
const params = new URLSearchParams({
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
token: verificationToken!.token,
|
||||
});
|
||||
|
||||
const verifyUrl = `${WEBAPP_URL}/api/auth/verify-email?${params.toString()}`;
|
||||
|
||||
await page.goto(verifyUrl);
|
||||
await page.getByTestId("secondary-email-action-group-button").nth(1).click();
|
||||
await page.getByTestId("secondary-email-make-primary-button").click();
|
||||
|
||||
expect(await page.getByTestId("profile-form-email-1-primary-badge").isVisible()).toEqual(true);
|
||||
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(false);
|
||||
});
|
||||
|
||||
test("Can resend verification link if the secondary email is unverified", async ({
|
||||
page,
|
||||
users,
|
||||
prisma,
|
||||
emails,
|
||||
}) => {
|
||||
const { secondaryEmail } = await createSecondaryEmail({ page, users });
|
||||
// When a user is created a link is sent, we will delete it manually to make sure verification link works fine
|
||||
await prisma.verificationToken.deleteMany({
|
||||
where: {
|
||||
identifier: secondaryEmail,
|
||||
},
|
||||
});
|
||||
const receivedEmails = await getEmailsReceivedByUser({ emails, userEmail: secondaryEmail });
|
||||
if (receivedEmails?.items?.[0]?.ID) {
|
||||
await emails.deleteMessage(receivedEmails.items[0].ID);
|
||||
}
|
||||
|
||||
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(true);
|
||||
await page.getByTestId("secondary-email-action-group-button").nth(1).click();
|
||||
expect(await page.locator("button[data-testid=resend-verify-email-button]").isDisabled()).toEqual(false);
|
||||
await page.getByTestId("resend-verify-email-button").click();
|
||||
|
||||
await testEmailVerificationLink({ page, prisma, emails, secondaryEmail });
|
||||
|
||||
const verificationToken = await prisma.verificationToken.findFirst({
|
||||
where: {
|
||||
identifier: secondaryEmail,
|
||||
},
|
||||
});
|
||||
await page.goto(`${WEBAPP_URL}/api/auth/verify-email?token=${verificationToken?.token}`);
|
||||
|
||||
await page.getByTestId("secondary-email-action-group-button").nth(1).click();
|
||||
expect(await page.locator("button[data-testid=resend-verify-email-button]").isVisible()).toEqual(false);
|
||||
expect(await page.getByTestId("profile-form-email-1-unverified-badge").isVisible()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
+563
-796
File diff suppressed because it is too large
Load Diff
@@ -47,6 +47,7 @@ test.describe("Workflow Tab - Event Type", () => {
|
||||
|
||||
await createWorkflow({ name: "A New Workflow", trigger: WorkflowTriggerEvents.NEW_EVENT });
|
||||
await page.goto(`/${user.username}/${eventType.slug}`);
|
||||
await page.click('[data-testid="incrementMonth"]');
|
||||
await bookEventOnThisPage(page);
|
||||
await assertWorkflowReminders(eventType.id, 1);
|
||||
});
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts",
|
||||
"verify_email_subject": "{{appName}}: Bestätigen Sie Ihr Konto",
|
||||
"check_your_email": "Schauen Sie in Ihrem E-Mail-Postfach nach",
|
||||
"old_email_address": "Alte E-Mail-Adresse",
|
||||
"new_email_address": "Neue E-Mail-Adresse",
|
||||
"verify_email_page_body": "Wir haben eine E-Mail an {{email}} gesendet. Es ist wichtig, Ihre E-Mail-Adresse zu bestätigen, um die Zustellbarkeit für E-Mails und Kalenderbenachrichtigungen von {{appName}} zu gewährleisten.",
|
||||
"verify_email_banner_body": "Bestätigen Sie Ihre E-Mail-Adresse, um die Zustellbarkeit von E-Mails und Kalenderbenachrichtigungen zu gewährleisten",
|
||||
"verify_email_email_header": "Bestätigen Sie Ihre E-Mail-Adresse",
|
||||
"verify_email_email_button": "E-Mail bestätigen",
|
||||
"verify_email_change_description": "Sie haben vor kurzem angefordert, die E-Mail-Adresse zu ändern, die Sie verwenden, um sich bei Ihrem {{appName}} Konto anzumelden. Bitte klicken Sie auf die Schaltfläche unten, um Ihre neue E-Mail-Adresse zu bestätigen.",
|
||||
"verify_email_change_success_toast": "Ihre E-Mail wurde auf {{email}} aktualisiert",
|
||||
"verify_email_change_failure_toast": "Fehler beim Aktualisieren der E-Mail.",
|
||||
"change_of_email": "Bestätigen Sie Ihre neue {{appName}} E-Mail-Adresse",
|
||||
"change_of_email_toast": "Wir haben einen Bestätigungslink an {{email}} gesendet. Wir werden Ihre E-Mail-Adresse aktualisieren, sobald Sie auf diesen Link klicken.",
|
||||
"copy_somewhere_safe": "Speichern Sie diesen API-Schlüssel an einem sicheren Ort. Sie können ihn nicht erneut ansehen.",
|
||||
"verify_email_email_body": "Bitte bestätigen Sie Ihre E-Mail-Adresse, indem Sie unten auf die Schaltfläche klicken.",
|
||||
"verify_email_by_code_email_body": "Bitte bestätigen Sie Ihre E-Mail-Adresse mit dem unten stehenden Code.",
|
||||
@@ -56,6 +63,17 @@
|
||||
"a_refund_failed": "Erstattung fehlgeschlagen",
|
||||
"awaiting_payment_subject": "Warte auf Zahlung: {{title}} am {{date}}",
|
||||
"meeting_awaiting_payment": "Zahlung für den Termin noch offen",
|
||||
"dark_theme_contrast_error": "Die Farbe des dunklen Designs hat die Kontrastprüfung nicht bestanden. Wir empfehlen Ihnen, diese Farbe zu ändern, damit Ihre Schaltflächen sichtbarer sind.",
|
||||
"light_theme_contrast_error": "Die Farbe des hellen Designs hat die Kontrastprüfung nicht bestanden. Wir empfehlen Ihnen, diese Farbe zu ändern, damit Ihre Schaltflächen sichtbarer sind.",
|
||||
"payment_not_created_error": "Zahlung konnte nicht erstellt werden",
|
||||
"couldnt_charge_card_error": "Karte konnte nicht für die Zahlung belastet werden",
|
||||
"no_available_users_found_error": "Keine verfügbaren Benutzer gefunden. Können Sie es mit einem anderen Zeitfenster versuchen?",
|
||||
"request_body_end_time_internal_error": "Interner Fehler. Request-Body enthält keinen Endzeitpunkt",
|
||||
"create_calendar_event_error": "Kalenderereignis konnte nicht im Kalender des Organisators erstellt werden",
|
||||
"update_calendar_event_error": "Kalenderereignis konnte nicht aktualisiert werden.",
|
||||
"delete_calendar_event_error": "Kalenderereignis konnte nicht gelöscht werden.",
|
||||
"already_signed_up_for_this_booking_error": "Sie sind bereits für diese Buchung registriert.",
|
||||
"hosts_unavailable_for_booking": "Einige der Veranstalter sind nicht für die Buchung verfügbar.",
|
||||
"help": "Hilfe",
|
||||
"price": "Preis",
|
||||
"paid": "Bezahlt",
|
||||
@@ -101,6 +119,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Aktion erforderlich - Neuplanung: Bitte buchen Sie eine neue Zeit für „{{eventType}}“ mit {{name}}",
|
||||
"hi_user_name": "Hallo, {{name}}",
|
||||
"ics_event_title": "{{eventType}} mit {{name}}",
|
||||
"please_book_a_time_sometime_later": "Ups, wir konnten Sie dieses Mal nicht verbinden. Bitte planen Sie stattdessen einen zukünftigen Anruf",
|
||||
"new_event_subject": "Neuer Termin: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "{{entryPoint}} beitreten",
|
||||
"notes": "Notizen",
|
||||
@@ -117,15 +136,20 @@
|
||||
"meeting_id": "Termin-ID",
|
||||
"meeting_password": "Termin-Passwort",
|
||||
"meeting_url": "Termin-URL",
|
||||
"meeting_url_not_found": "Meeting-URL nicht gefunden",
|
||||
"token_not_found": "Token nicht gefunden",
|
||||
"some_other_host_already_accepted_the_meeting": "Ein anderer Veranstalter hat das Meeting bereits akzeptiert. Möchten Sie trotzdem beitreten? <1>Mit dem Meeting fortfahren</1>",
|
||||
"meeting_request_rejected": "Ihre Terminanfrage wurde abgelehnt",
|
||||
"rejected_event_type_with_organizer": "Abgelehnt: {{eventType}} mit {{organizer}} auf {{date}}",
|
||||
"hi": "Hi",
|
||||
"join_team": "Team beitreten",
|
||||
"manage_this_team": "Dieses Team verwalten",
|
||||
"team_info": "Informationen zum Team",
|
||||
"join_meeting": "Meeting beitreten",
|
||||
"request_another_invitation_email": "Falls Sie {{toEmail}} lieber nicht als Ihre {{appName}}-E-Mail verwenden möchten oder bereits über ein {{appName}}-Konto verfügen, fordern Sie bitte eine weitere Einladung zu dieser E-Mail an.",
|
||||
"you_have_been_invited": "Sie wurden eingeladen, dem Team {{teamName}} beizutreten",
|
||||
"user_invited_you": "{{user}} hat Sie eingeladen, dem {{entity}} {{team}} auf {{appName}} beizutreten",
|
||||
"user_invited_you_to_subteam": "{{user}} hat Sie eingeladen, dem Team {{team}} der Organisation {{parentTeamName}} auf {{appName}} beizutreten",
|
||||
"hidden_team_member_title": "Sie sind in diesem Team ausgeblendet",
|
||||
"hidden_team_member_message": "Ihre Teammitgliedschaft ist nicht bezahlt, entweder upgraden Sie Ihren Account auf Pro oder lassen den Team-Organisator wissen, dass er für Ihren Platz im Team bezahlen kann.",
|
||||
"hidden_team_owner_message": "Sie benötigen einen Pro-Account um die Teamfunktionen zu verweden. Sie werden dem Team nicht angezeigt, bis der Account upgegradet wurde.",
|
||||
@@ -229,6 +253,7 @@
|
||||
"reset_your_password": "Legen Sie Ihr neues Passwort mit den Anweisungen fest, die an Ihre E-Mail-Adresse gesendet wurden.",
|
||||
"email_change": "Melden Sie sich mit Ihrer neuen E-Mail-Adresse und Ihrem Passwort wieder an.",
|
||||
"create_your_account": "Erstellen Sie Ihr Konto",
|
||||
"create_your_calcom_account": "Erstellen Sie Ihr Cal.com-Konto",
|
||||
"sign_up": "Registrieren",
|
||||
"youve_been_logged_out": "Sie wurden abgemeldet",
|
||||
"hope_to_see_you_soon": "Wir hoffen, Sie bald wieder zu sehen!",
|
||||
@@ -266,6 +291,9 @@
|
||||
"nearly_there_instructions": "Zu guter Letzt: Ein Foto und eine Kurzbeschreibung zu Ihnen helfen, öfter gebucht zu werden, da Kunden dann genauer wissen, bei wem sie einen Termin buchen.",
|
||||
"set_availability_instructions": "Definieren Sie Zeiträume, wenn Sie wiederkehrend verfügbar sind. Sie können später weitere Zeiträume erstellen und verschiedenen Kalendern zuordnen.",
|
||||
"set_availability": "Verfügbarkeit festlegen",
|
||||
"set_availbility_description": "Legen Sie Zeitpläne für die Zeiten fest, in denen Sie gebucht werden möchten.",
|
||||
"share_a_link_or_embed": "Link oder Einbettung teilen",
|
||||
"share_a_link_or_embed_description": "Teilen Sie Ihren {{appName}} Link oder betten Sie ihn auf Ihrer Website ein.",
|
||||
"availability_settings": "Verfügbarkeitseinstellungen",
|
||||
"continue_without_calendar": "Ohne Kalender fortfahren",
|
||||
"continue_with": "Mit {{appName}} fortfahren",
|
||||
@@ -281,6 +309,7 @@
|
||||
"welcome_to_calcom": "Willkommen bei {{appName}}",
|
||||
"welcome_instructions": "Wie ist Ihr Name und in welcher Zeitzone sind Sie? Dies kann später bearbeitet werden.",
|
||||
"connect_caldav": "Mit CalDav-Server verbinden",
|
||||
"connect_ics_feed": "Mit einem ICS-Feed verbinden",
|
||||
"connect": "Verbinden",
|
||||
"try_for_free": "Kostenlos testen",
|
||||
"create_booking_link_with_calcom": "Erstellen Sie Ihren eigenen Buchungslink mit {{appName}}",
|
||||
@@ -419,6 +448,7 @@
|
||||
"browse_api_documentation": "Durchsuchen Sie unsere API Dokumentation",
|
||||
"leverage_our_api": "Nutzen Sie unsere API für die volle Kontrolle und Anpassbarkeit.",
|
||||
"create_webhook": "Webhook erstellen",
|
||||
"instant_meeting": "Sofortiges Meeting erstellt",
|
||||
"booking_cancelled": "Termin abgesagt",
|
||||
"booking_rescheduled": "Termin verlegt",
|
||||
"recording_ready": "Download-Link der Aufnahme ist nun verfügbar",
|
||||
@@ -533,6 +563,7 @@
|
||||
"enter_number_between_range": "Bitte geben Sie eine Zahl zwischen 1 und {{maxOccurences}} ein",
|
||||
"email_address": "E-Mail Adresse",
|
||||
"enter_valid_email": "Bitte geben Sie eine gültige Emaildresse ein",
|
||||
"please_schedule_future_call": " Bitte planen Sie einen Anruf in der Zukunft ein, falls wir nicht innerhalb von {{seconds}} Sekunden verfügbar sind",
|
||||
"location": "Ort",
|
||||
"address": "Adresse",
|
||||
"enter_address": "Geben Sie eine Adresse ein",
|
||||
@@ -592,6 +623,7 @@
|
||||
"new_member": "Neues Mitglied",
|
||||
"invite": "Einladen",
|
||||
"add_team_members": "Teammitglieder hinzufügen",
|
||||
"add_org_members": "Mitglieder hinzufügen",
|
||||
"add_team_members_description": "Lade andere ein, deinem Team beizutreten",
|
||||
"add_team_member": "Teammitglied hinzufügen",
|
||||
"invite_new_member": "Ein neues Mitglied einladen",
|
||||
@@ -606,8 +638,9 @@
|
||||
"hide_book_a_team_member_description": "Blendet die „Ein Teammitglied buchen“-Schaltflächen auf Ihren öffentlichen Seiten aus.",
|
||||
"danger_zone": "Achtung",
|
||||
"account_deletion_cannot_be_undone": "Vorsicht. Löschen eines Kontos kann nicht rückgängig gemacht werden.",
|
||||
"team_deletion_cannot_be_undone": "Seien Sie vorsichtig. Das Löschen eines Teams kann nicht rückgängig gemacht werden",
|
||||
"back": "Zurück",
|
||||
"cancel": "Absagen",
|
||||
"cancel": "Abbrechen",
|
||||
"cancel_all_remaining": "Alle verbleibenden absagen",
|
||||
"apply": "Anwenden",
|
||||
"cancel_event": "Termin absagen",
|
||||
@@ -656,6 +689,7 @@
|
||||
"default_duration": "Standardlaufzeit",
|
||||
"default_duration_no_options": "Bitte wählen Sie zuerst die verfügbaren Laufzeiten aus",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"minutes": "Minuten",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Treffen zwischen mehreren Teammitgliedern durchwechseln.",
|
||||
@@ -668,6 +702,7 @@
|
||||
"add_members": "Mitglieder hinzufügen...",
|
||||
"no_assigned_members": "Keine zugeordneten Mitglieder",
|
||||
"assigned_to": "Zugeordnet zu",
|
||||
"you_must_be_logged_in_to": "Sie müssen bei {{url}} angemeldet sein",
|
||||
"start_assigning_members_above": "Mit der Zuordnung von Mitgliedern oben beginnen",
|
||||
"locked_fields_admin_description": "Mitglieder werden dies nicht bearbeiten können",
|
||||
"locked_fields_member_description": "Diese Option wurde vom Team-Admin gesperrt",
|
||||
@@ -772,6 +807,7 @@
|
||||
"additional_input_description": "Erfordert die Eingabe zusätzlicher Eingaben durch den Planer vor der Terminbestätigung",
|
||||
"label": "Bezeichnung",
|
||||
"placeholder": "Platzhalter",
|
||||
"display_add_to_calendar_organizer": "„Zum Kalender hinzufügen“-E-Mail als Organisator anzeigen",
|
||||
"type": "Typ",
|
||||
"edit": "Bearbeiten",
|
||||
"add_input": "Eingabe hinzufügen",
|
||||
@@ -780,6 +816,9 @@
|
||||
"requires_confirmation_description": "Die Buchung muss manuell bestätigt werden, bevor die Buchungsinformationen an die Integrationen weitergeleitet werden und eine Bestätigungsmail gesendet wird.",
|
||||
"recurring_event": "Wiederkehrender Termin",
|
||||
"recurring_event_description": "Personen können sich für wiederkehrende Termine anmelden",
|
||||
"cannot_be_used_with_paid_event_types": "Es kann nicht mit bezahlten Ereignistypen verwendet werden",
|
||||
"warning_payment_instant_meeting_event": "Softige Meetings werden von wiederkehrenden Ereignissen und Zahlungs-Apps noch nicht unterstützt",
|
||||
"warning_instant_meeting_experimental": "Experimentell: „Sofortiges Meeting“-Ereignisse sind derzeit eine experimentelle Funktion.",
|
||||
"starting": "Beginnen",
|
||||
"disable_guests": "Gäste deaktivieren",
|
||||
"disable_guests_description": "Das Hinzufügen zusätzlicher Gäste deaktivieren.",
|
||||
@@ -788,6 +827,7 @@
|
||||
"private_link_label": "Privater Link",
|
||||
"private_link_hint": "Ihr privater Link wird nach jeder Nutzung neu generiert",
|
||||
"copy_private_link": "Privaten Link kopieren",
|
||||
"copy_private_link_to_event": "Privaten Link zum Ereignis kopieren",
|
||||
"private_link_description": "Erzeugen Sie einen privaten Link, den Sie teilen können, ohne Ihren {{appName}}-Benutzername preiszugeben",
|
||||
"invitees_can_schedule": "Teilnehmende können folgendes buchen",
|
||||
"date_range": "Datumszeitraum",
|
||||
@@ -847,6 +887,7 @@
|
||||
"next_step": "Schritt überspringen",
|
||||
"prev_step": "Vorheriger Schritt",
|
||||
"install": "Installieren",
|
||||
"start_paid_trial": "Kostenlose Testphase starten",
|
||||
"installed": "Installiert",
|
||||
"active_install_one": "{{count}} aktive Installation",
|
||||
"active_install_other": "{{count}} aktive Installationen",
|
||||
@@ -861,6 +902,7 @@
|
||||
"toggle_calendars_conflict": "Wechseln Sie die Kalender in denen nach Konflikten gesucht werden sollen, um Doppelbuchungen zu vermeiden.",
|
||||
"connect_additional_calendar": "Zusätzlichen Kalender verbinden",
|
||||
"calendar_updated_successfully": "Kalender erfolgreich aktualisiert",
|
||||
"check_here": "Hier auswählen",
|
||||
"conferencing": "Videokonferenzen",
|
||||
"calendar": "Kalender",
|
||||
"payments": "Zahlungen",
|
||||
@@ -1032,6 +1074,9 @@
|
||||
"user_impersonation_heading": "Benutzeridentitätswechsel",
|
||||
"user_impersonation_description": "Erlaubt es unserem Support-Team, sich vorübergehend mit Ihrem Konto anzumelden, um jegliche Probleme, die Sie gemeldet haben, schneller zu lösen.",
|
||||
"team_impersonation_description": "Ermöglicht den Team-Administratoren, sich vorübergehend als Sie einzuloggen.",
|
||||
"cal_signup_description": "Für Einzelpersonen kostenlos. Teampläne für kollaborative Funktionen.",
|
||||
"make_org_private": "Organisation privat stellen",
|
||||
"make_org_private_description": "Wenn diese Einstellung aktiv ist, können Ihre Organisationsmitglieder keine anderen Organisationsmitglieder sehen.",
|
||||
"make_team_private": "Team privat stellen",
|
||||
"make_team_private_description": "Wenn diese Einstellung aktiv ist, können deine Teammitglieder keine anderen Teammitglieder sehen.",
|
||||
"you_cannot_see_team_members": "Sie können nicht alle Teammitglieder eines privaten Teams sehen.",
|
||||
@@ -1091,6 +1136,7 @@
|
||||
"developer_documentation": "Entwickler-Dokumentation",
|
||||
"get_in_touch": "Kontakt aufnehmen",
|
||||
"contact_support": "Support kontaktieren",
|
||||
"premium_support": "Premium-Support",
|
||||
"community_support": "Community Support",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Vielen Dank für Ihr Feedback!",
|
||||
@@ -1254,6 +1300,7 @@
|
||||
"default_calendar_selected": "Standardkalender",
|
||||
"hide_from_profile": "Im Profil ausblenden",
|
||||
"event_setup_tab_title": "Termin-Einrichtung",
|
||||
"availability_not_found_in_schedule_error": "Keine Verfügbarkeit im Zeitplan gefunden",
|
||||
"event_limit_tab_title": "Limits",
|
||||
"event_limit_tab_description": "Wie oft können Sie gebucht werden",
|
||||
"event_advanced_tab_description": "Kalendereinstellungen & mehr...",
|
||||
@@ -1274,6 +1321,7 @@
|
||||
"do_this": "Mache dies",
|
||||
"turn_off": "Abschalten",
|
||||
"turn_on": "Einschalten",
|
||||
"cancelled_bookings_cannot_be_rescheduled": "Stornierte Buchungen können nicht verschoben werden",
|
||||
"settings_updated_successfully": "Einstellungen erfolgreich aktualisiert",
|
||||
"error_updating_settings": "Fehler beim Aktualisieren der Einstellungen",
|
||||
"personal_cal_url": "Meine persönliche {{appName}}-URL",
|
||||
@@ -1297,6 +1345,7 @@
|
||||
"customize_your_brand_colors": "Passen Sie Ihre eigene Markenfarbe auf Ihrer Buchungsseite an.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Entfernt alle {{appName}}-bezogenen Markenzeichen, z. B. „Powered by {{appName}}.“",
|
||||
"instant_meeting_with_title": "Sofortiges Meeting mit {{name}}",
|
||||
"profile_picture": "Profilbild",
|
||||
"upload": "Upload",
|
||||
"add_profile_photo": "Profilbild hinzufügen",
|
||||
@@ -1352,6 +1401,7 @@
|
||||
"event_name_info": "Name des Ereignistyps",
|
||||
"event_date_info": "Das Datum der Veranstaltung",
|
||||
"event_time_info": "Die Startzeit des Termins",
|
||||
"event_type_not_found": "Ereignistyp nicht gefunden",
|
||||
"location_variable": "Ort",
|
||||
"location_info": "Der Ort des Events",
|
||||
"additional_notes_variable": "Zusätzliche Notizen",
|
||||
@@ -1394,6 +1444,7 @@
|
||||
"slot_length": "Zeitfensterlänge",
|
||||
"booking_appearance": "Darstellung der Buchungsseite",
|
||||
"appearance_team_description": "Einstellungen für die Buchungsdarstellung Ihres Teams verwalten",
|
||||
"appearance_org_description": "Einstellungen für die Buchungsdarstellung Ihrer Organisation verwalten",
|
||||
"only_owner_change": "Nur der Besitzer dieses Teams kann Änderungen an den Buchungen des Teams vornehmen ",
|
||||
"team_disable_cal_branding_description": "Entfernt alle {{appName}}-bezogenen Markenzeichen, z. B. „Powered by {{appName}}“",
|
||||
"invited_by_team": "{{teamName}} hat Sie eingeladen, dem Team als {{role}} beizutreten",
|
||||
@@ -1447,6 +1498,7 @@
|
||||
"password_updated": "Passwort aktualisiert!",
|
||||
"pending_payment": "Zahlung ausstehend",
|
||||
"pending_invites": "Offene Einladungen",
|
||||
"pending_organization_invites": "Ausstehende Organisationseinladungen",
|
||||
"not_on_cal": "Nicht auf {{appName}}",
|
||||
"no_calendar_installed": "Kein Kalender installiert",
|
||||
"no_calendar_installed_description": "Sie haben noch keinen Ihrer Kalender verbunden",
|
||||
@@ -1458,6 +1510,8 @@
|
||||
"report_app": "App melden",
|
||||
"limit_booking_frequency": "Buchungsfrequenz begrenzen",
|
||||
"limit_booking_frequency_description": "Begrenzen, wie oft dieses Ereignis gebucht werden kann",
|
||||
"limit_booking_only_first_slot": "Buchun auf den ersten Platz beschränken",
|
||||
"limit_booking_only_first_slot_description": "Nur die Buchung des ersten Platzes am Tag erlauben",
|
||||
"limit_total_booking_duration": "Gesamtbuchungsdauer beschränken",
|
||||
"limit_total_booking_duration_description": "Begrenzt die Gesamtanzahl, die dieser Termin gebucht werden kann",
|
||||
"add_limit": "Begrenzung hinzufügen",
|
||||
@@ -1525,10 +1579,14 @@
|
||||
"your_org_disbanded_successfully": "Ihre Organization wurde erfolgreich aufgelöst",
|
||||
"error_creating_team": "Fehler beim Erstellen des Teams",
|
||||
"you": "Sie",
|
||||
"or_continue_with": "Oder fortfahren mit",
|
||||
"resend_email": "E-Mail erneut senden",
|
||||
"member_already_invited": "Mitglied wurde bereits eingeladen",
|
||||
"already_in_use_error": "Benutzername wird bereits verwendet",
|
||||
"enter_email_or_username": "E-Mail oder Benutzername eingeben",
|
||||
"enter_email": "E-Mail-Adresse eingeben",
|
||||
"enter_emails": "E-Mail-Adressen eingeben",
|
||||
"too_many_invites": "Sie sind darauf beschränkt, maximal {{nbUsers}} Benutzer gleichzeitig einzuladen.",
|
||||
"team_name_taken": "Dieser Name ist bereits vergeben",
|
||||
"must_enter_team_name": "Team-Name muss eingegeben werden",
|
||||
"team_url_required": "Team-URL muss eingegeben werden",
|
||||
@@ -1608,6 +1666,7 @@
|
||||
"individual": "Person",
|
||||
"all_bookings_filter_label": "Alle Buchungen",
|
||||
"all_users_filter_label": "Alle Benutzer",
|
||||
"all_event_types_filter_label": "Alle Ereignistypen",
|
||||
"your_bookings_filter_label": "Ihre Buchungen",
|
||||
"meeting_url_variable": "Meeting-URL",
|
||||
"meeting_url_info": "Die URL der Meeting-Konferenz",
|
||||
@@ -1704,9 +1763,11 @@
|
||||
"organizer_timezone": "Zeitzone des Veranstalters",
|
||||
"email_user_cta": "Einladung anzeigen",
|
||||
"email_no_user_invite_heading_team": "Sie wurden eingeladen, einem {{appName}}-Team beizutreten",
|
||||
"email_no_user_invite_heading_subteam": "Sie wurden eingeladen, einem Team der Organisation {{parentTeamName}} beizutreten",
|
||||
"email_no_user_invite_heading_org": "Sie wurden eingeladen, einer {{appName}}-Organisation beizutreten",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} hat Sie eingeladen, dem Team in {{appName}} beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne ständiges hin und her zu planen.",
|
||||
"email_user_invite_subheading_team": "{{invitedBy}} hat Sie eingeladen, dem Team „{{teamName}}“ beizutreten. {{appName}} ist der Event-Planer, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne Hin und Her zu planen.",
|
||||
"email_user_invite_subheading_subteam": "{{invitedBy}} hat Sie eingeladen, dem Team „{{teamName}}“ in seiner bzw. ihrer Organisation {{parentTeamName}} bei {{appName}} beizutreten. {{appName}} ist der Terminplaner, der es Ihnen und Ihrem Team ermöglicht, Meetings ohne Hin und Her zu planen.",
|
||||
"email_user_invite_subheading_org": "{{invitedBy}} hat Sie eingeladen, ihrer Organisation „{{teamName}}“ in {{appName}} beizutreten. {{appName}} ist der Terminplaner, der es Ihnen und Ihrer Organisation ermöglicht, Meetings ohne ständiges hin und her zu planen.",
|
||||
"email_no_user_invite_steps_intro": "Wir begleiten Sie durch ein paar Schritte, danach können Sie in kürzester Zeit stressfreie Planung mit Ihrer {{entity}} genießen.",
|
||||
"email_no_user_step_one": "Wählen Sie Ihren Benutzernamen",
|
||||
@@ -1839,7 +1900,9 @@
|
||||
"review_event_type": "Ereignistyp überprüfen",
|
||||
"looking_for_more_analytics": "Suchen Sie nach weiteren Analysedaten?",
|
||||
"looking_for_more_insights": "Möchten Sie mehr Insights?",
|
||||
"filters": "Filter",
|
||||
"add_filter": "Filter hinzufügen",
|
||||
"remove_filters": "Alle Filter löschen",
|
||||
"select_user": "Benutzer auswählen",
|
||||
"select_event_type": "Ereignistyp auswählen",
|
||||
"select_date_range": "Datumsbereich auswählen",
|
||||
@@ -1930,6 +1993,7 @@
|
||||
"connect_google_workspace": "Google Workspace verbinden",
|
||||
"google_workspace_admin_tooltip": "Sie müssen ein Workspace-Admin sein, um diese Funktion nutzen zu können",
|
||||
"first_event_type_webhook_description": "Erstellen Sie Ihren ersten Webhook für diesen Termintypen",
|
||||
"create_instant_meeting_webhook_description": "Erstellen Sie Ihren ersten Webhook mit „Sofortiges Meeting erstellt“ als Auslöser für diesen Ereignistyp",
|
||||
"install_app_on": "App installieren auf",
|
||||
"create_for": "Erstellen für",
|
||||
"currency": "Währung",
|
||||
@@ -1994,6 +2058,8 @@
|
||||
"add_times_to_your_email": "Zeiten zur E-Mail hinzufügen",
|
||||
"select_time": "Zeit auswählen",
|
||||
"select_date": "Datum auswählen",
|
||||
"connecting_you_to_someone": "Wir verbinden Sie mit jemandem.",
|
||||
"please_do_not_close_this_tab": "Diesen Tab bitte nicht schließen",
|
||||
"see_all_available_times": "Alle verfügbaren Zeiten ansehen",
|
||||
"org_team_names_example_1": "z.B. Marketing-Team",
|
||||
"org_team_names_example_2": "z.B. Vertriebsteam",
|
||||
@@ -2011,8 +2077,12 @@
|
||||
"description_requires_booker_email_verification": "Um sicherzustellen, dass die E-Mail des Buchenden vor der Terminplanung verifiziert ist",
|
||||
"requires_confirmation_mandatory": "Textnachrichten können nur an Teilnehmer gesendet werden, wenn der Ereignistyp bestätigt werden muss.",
|
||||
"organizations": "Organisationen",
|
||||
"upload_cal_video_logo": "Cal Video Logo hochladen",
|
||||
"update_cal_video_logo": "Cal Video Logo aktualisieren",
|
||||
"cal_video_logo_upload_instruction": "Um sicherzustellen, dass Ihr Logo vor dem dunklen Hintergrund von Cal Video gut sichtbar ist, laden Sie bitte ein helles Bild im PNG- oder SVG-Format hoch, um die Transparenz zu erhalten.",
|
||||
"org_admin_other_teams": "Weitere Teams",
|
||||
"org_admin_other_teams_description": "Hier können Sie Teams innerhalb Ihrer Organisation sehen, zu denen Sie nicht gehören. Sie können sich bei Bedarf selbst hinzufügen.",
|
||||
"not_part_of_org": "Sie gehören keiner Organisation an",
|
||||
"no_other_teams_found": "Keine weiteren Teams gefunden",
|
||||
"no_other_teams_found_description": "Es gibt keine weiteren Teams in dieser Organisation.",
|
||||
"attendee_first_name_variable": "Vorname des Teilnehmers",
|
||||
@@ -2048,7 +2118,9 @@
|
||||
"org_error_processing": "Beim Verarbeiten dieser Organisation ist ein Fehler aufgetreten",
|
||||
"orgs_page_description": "Eine Liste aller Organisationen. Wenn Sie eine Organisation akzeptieren, können sich alle Benutzer mit dieser E-Mail-Domain OHNE E-Mail-Überprüfung registrieren.",
|
||||
"unverified": "Nicht verifiziert",
|
||||
"verified": "Verifiziert",
|
||||
"dns_missing": "DNS fehlt",
|
||||
"dns_configured": "DNS konfiguriert",
|
||||
"mark_dns_configured": "Als DNS konfiguriert kennzeichnen",
|
||||
"value": "Wert",
|
||||
"your_organization_updated_sucessfully": "Ihre Organisation wurde erfolgreich aktualisiert",
|
||||
@@ -2058,8 +2130,11 @@
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Kürzlich hinzugefügt",
|
||||
"connect_all_calendars": "Verbinden Sie alle Ihre Kalender",
|
||||
"connect_all_calendars_description": "{{appName}} liest die Verfügbarkeit aus allen Ihren vorhandenen Kalendern.",
|
||||
"workflow_automation": "Workflow-Automatisierung",
|
||||
"workflow_automation_description": "Personalisieren Sie Ihre Planungserfahrung mit Workflows",
|
||||
"scheduling_for_your_team": "Workflow-Automatisierung",
|
||||
"scheduling_for_your_team_description": "Planen Sie Ihr Team mit kollektiver Terminplanung und Round-Robin-Terminplanung",
|
||||
"no_members_found": "Keine Mitglieder gefunden",
|
||||
"event_setup_length_error": "Ereignis-Einrichtung: Die Dauer muss mindestens 1 Minute betragen.",
|
||||
"availability_schedules": "Verfügbarkeitspläne",
|
||||
@@ -2092,10 +2167,100 @@
|
||||
"overlay_my_calendar": "Meinen Kalender überlagern",
|
||||
"overlay_my_calendar_toc": "Durch das Verbinden mit Ihrem Kalender akzeptieren Sie unsere Datenschutzerklärung und Nutzungsbedingungen. Sie können den Zugriff jederzeit widerrufen.",
|
||||
"view_overlay_calendar_events": "Sehen Sie sich Ihre Kalendertermine an, um Buchungskonflikte zu vermeiden.",
|
||||
"join_event_location": "{{eventLocationType}} beitreten",
|
||||
"troubleshooting": "Problembehandlung",
|
||||
"calendars_were_checking_for_conflicts": "Kalender, die wir auf Konflikten überprüfen",
|
||||
"availabilty_schedules": "Verfügbarkeitspläne",
|
||||
"manage_calendars": "Kalender verwalten",
|
||||
"manage_availability_schedules": "Verfügbarkeitspläne verwalten",
|
||||
"lock_timezone_toggle_on_booking_page": "Zeitzone auf der Buchungsseite sperren",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Um die Zeitzone auf der Buchungsseite zu sperren, nützlich für Termine in Person.",
|
||||
"event_setup_multiple_payment_apps_error": "Sie können nur eine Zahlungs-App pro Ereignistyp aktiv haben.",
|
||||
"number_in_international_format": "Bitte geben Sie die Nummer im internationalen Format ein.",
|
||||
"install_calendar": "Kalender installieren",
|
||||
"branded_subdomain": "Marken-Subdomain",
|
||||
"branded_subdomain_description": "Holen Sie sich Ihre eigene Marken-Subdomain, wie z.B. acme.cal.com",
|
||||
"org_insights": "Organisationsweite Insights",
|
||||
"org_insights_description": "Verstehen Sie, wie Ihre gesamte Organisation Zeit verbringt",
|
||||
"extensive_whitelabeling": "Dedizierte Onboarding- und Engineeringsupport",
|
||||
"extensive_whitelabeling_description": "Passen Sie Ihre Planungserfahrung mit Ihrem eigenen Logo, eigenen Farben und mehr an",
|
||||
"unlimited_teams": "Unbegrenzte Teams",
|
||||
"unlimited_teams_description": "Fügen Sie Ihrer Organisation so viele Unterteams hinzu, wie notwendig",
|
||||
"unified_billing": "Einheitliche Abrechnung",
|
||||
"unified_billing_description": "Fügen Sie eine einzige Kreditkarte hinzu, um alle Abos Ihres Teams zu bezahlen",
|
||||
"advanced_managed_events": "Erweiterte verwaltete Ereignistypen",
|
||||
"advanced_managed_events_description": "Fügen Sie eine einzige Kreditkarte hinzu, um alle Abonnements Ihres Teams zu bezahlen",
|
||||
"enterprise_description": "Rüsten Sie auf Enterprise auf, um Ihre Organisation zu erstellen",
|
||||
"create_your_org": "Erstellen Sie Ihre Organisation",
|
||||
"create_your_org_description": "Rüsten Sie auf Enterprise auf und erhalten Sie eine Subdomain, einheitliche Abrechnung, Insights, umfangreiches Whitelabeling und mehr",
|
||||
"other_payment_app_enabled": "Sie können nur eine Zahlungs-App pro Ereignistyp aktivieren",
|
||||
"admin_delete_organization_description": "<ul><li>Teams, die Mitglieder dieser Organisation sind, werden ebenfalls zusammen mit ihren Ereignistypen gelöscht</li><li>Benutzer, die Teil der Organisation waren, werden nicht gelöscht und ihre Ereignistypen bleiben ebenfalls intakt.</li><li>Benutzernamen würden geändert werden, um zu ermöglichen, dass sie außerhalb der Organisation existieren können</li></ul>",
|
||||
"admin_delete_organization_title": "{{organizationName}} löschen?",
|
||||
"published": "Veröffentlicht",
|
||||
"unpublished": "Unveröffentlicht",
|
||||
"publish": "Veröffentlichen",
|
||||
"org_publish_error": "Organisation konnte nicht veröffentlicht werden",
|
||||
"troubleshooter_tooltip": "Öffnen Sie die Problembehandlung und finden Sie heraus, was mit Ihrem Zeitplan nicht stimmt",
|
||||
"need_help": "Brauchst du Hilfe?",
|
||||
"troubleshooter": "Problembehandlung",
|
||||
"please_install_a_calendar": "Bitte installieren Sie einen Kalender",
|
||||
"instant_tab_title": "Sofortige Buchung",
|
||||
"instant_event_tab_description": "Personen sofort buchen lassen",
|
||||
"uprade_to_create_instant_bookings": "Rüsten Sie auf Enterprise auf und lassen Sie Ihre Gäste an einem Sofortanruf teilnehmen, an dem die Teilnehmer direkt teilnehmen können. Dies ist nur für Team-Ereignistypen verfügbar",
|
||||
"dont_want_to_wait": "Sie möchten nicht warten?",
|
||||
"meeting_started": "Meeting gestartet",
|
||||
"pay_and_book": "Zum Buchen bezahlen",
|
||||
"booking_not_found_error": "Buchung konnte nicht gefunden werden",
|
||||
"booking_seats_full_error": "Die Buchungsplätze sind voll",
|
||||
"missing_payment_credential_error": "Fehlende Zahlungsinformationen",
|
||||
"missing_payment_app_id_error": "Fehlende Zahlungsapp-ID",
|
||||
"not_enough_available_seats_error": "Die Buchung hat nicht genügend Plätze verfügbar",
|
||||
"user_redirect_title": "{{username}} ist derzeit kurzzeitig abwesend.",
|
||||
"user_redirect_description": "In der Zwischenzeit wird {{profile.username}} für alle neuen geplanten Meetings im Namen von {{username}} verantwortlich sein.",
|
||||
"out_of_office": "Abwesend",
|
||||
"out_of_office_description": "Konfigurieren Sie Aktionen in Ihrem Profil während Sie abwesend sind.",
|
||||
"send_request": "Anfrage senden",
|
||||
"start_date_and_end_date_required": "Startdatum und Enddatum sind erforderlich",
|
||||
"start_date_must_be_before_end_date": "Das Startdatum muss vor dem Enddatum liegen",
|
||||
"start_date_must_be_in_the_future": "Das Startdatum muss in der Zukunft liegen",
|
||||
"user_not_found": "Benutzer nicht gefunden",
|
||||
"out_of_office_entry_already_exists": "Abwesenheitseintrag existiert bereits",
|
||||
"out_of_office_id_required": "Abwesenheitseintrags-ID erforderlich",
|
||||
"booking_redirect_infinite_not_allowed": "Es besteht bereits eine Buchungsumleitung von diesem Benutzer zu Ihnen.",
|
||||
"success_entry_created": "Neuer Eintrag wurde erfolgreich erstellt",
|
||||
"booking_redirect_email_subject": "Buchungsumleitungsbenachrichtigung",
|
||||
"booking_redirect_email_title": "Buchungsumleitungsbenachrichtigung",
|
||||
"booking_redirect_email_description": "Sie haben eine Umleitung der Buchung von {{toName}} erhalten, so dass deren Profillinks für das Zeitfenster an Sie umgeleitet werden: ",
|
||||
"success_accept_booking_redirect": "Sie haben diese Buchungsumleitungsanfrage akzeptiert.",
|
||||
"success_reject_booking_redirect": "Sie haben diese Buchungsumleitungsanfrage abgelehnt.",
|
||||
"copy_link_booking_redirect_request": "Link zum Teilen der Anfrage kopieren",
|
||||
"booking_redirect_request_title": "Buchungsumleitungsanfrage",
|
||||
"select_team_member": "Teammitglied auswählen",
|
||||
"going_away_title": "Verreisen Sie? Markieren Sie einfach Ihren Profillink für eine gewisse Zeit als nicht verfügbar.",
|
||||
"redirect_team_enabled": "Leiten Sie Ihr Profil an ein anderes Teammitglied um",
|
||||
"redirect_team_disabled": "Leiten Sie Ihr Profil an ein anderes Teammitglied um (Teamplan erforderlich)",
|
||||
"out_of_office_unavailable_list": "Abwesenheits-Unverfügbarkeitsliste",
|
||||
"success_deleted_entry_out_of_office": "Eintrag erfolgreich gelöscht",
|
||||
"temporarily_out_of_office": "Temporär abwesend?",
|
||||
"add_a_redirect": "Umleitung hinzufügen",
|
||||
"create_entry": "Eintrag erstellen",
|
||||
"time_range": "Zeitspanne",
|
||||
"automatically_add_all_team_members": "Alle Teammitglieder hinzufügen, einschließlich zukünftiger Mitglieder",
|
||||
"redirect_to": "Umleiten zu",
|
||||
"having_trouble_finding_time": "Haben Sie Schwierigkeiten, ein Zeitfenster zu finden?",
|
||||
"show_more": "Mehr anzeigen",
|
||||
"assignment_description": "Planen Sie Meetings ein, wenn jeder verfügbar ist, oder rotieren Sie durch die Mitglieder Ihres Team",
|
||||
"lowest": "Niedrigste",
|
||||
"low": "Niedrig",
|
||||
"medium": "Mittel",
|
||||
"high": "Hoch",
|
||||
"Highest": "Höchste",
|
||||
"send_booker_to": "Bucher senden an",
|
||||
"set_priority": "Priorität festlegen",
|
||||
"priority_for_user": "Priorität von {{userName}}",
|
||||
"change_priority": "Priorität ändern",
|
||||
"field_identifiers_as_variables": "Verwenden Sie Feldbezeichner als Variablen für Ihre benutzerdefinierte Ereignisumleitung",
|
||||
"field_identifiers_as_variables_with_example": "Verwenden Sie Feldbezeichner als Variablen für Ihre benutzerdefinierte Ereignisumleitung (z. B. {{variable}})",
|
||||
"account_already_linked": "Konto ist bereits verknüpft",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -638,7 +638,7 @@
|
||||
"hide_book_a_team_member_description": "Hide Book a Team Member Button from your public pages.",
|
||||
"danger_zone": "Danger zone",
|
||||
"account_deletion_cannot_be_undone":"Be Careful. Account deletion cannot be undone.",
|
||||
"team_deletion_cannot_be_undone":"Be Careful. Team deletion cannot be undone",
|
||||
"team_deletion_cannot_be_undone":"Be Careful. Team deletion cannot be undone.",
|
||||
"back": "Back",
|
||||
"cancel": "Cancel",
|
||||
"cancel_all_remaining": "Cancel all remaining",
|
||||
@@ -808,6 +808,7 @@
|
||||
"label": "Label",
|
||||
"placeholder": "Placeholder",
|
||||
"display_add_to_calendar_organizer":"Display 'Add to calendar' email as the organizer",
|
||||
"reconnect_calendar_to_use": "Please note that you may need to disconnect and then reconnect your 'Add to Calendar' account to use this feature.",
|
||||
"type": "Type",
|
||||
"edit": "Edit",
|
||||
"add_input": "Add an Input",
|
||||
@@ -1472,8 +1473,10 @@
|
||||
"route_to_the_right_person": "Route to the right person based on the answers to your form",
|
||||
"form_description": "Create your form to route a booker",
|
||||
"copy_link_to_form": "Copy link to form",
|
||||
"theme": "Theme",
|
||||
"theme": "Booking Page Theme",
|
||||
"theme_applies_note": "This only applies to your public booking pages",
|
||||
"app_theme": "Dashboard Theme",
|
||||
"app_theme_applies_note": "This only applies to your logged in dashboard",
|
||||
"theme_system": "System default",
|
||||
"add_a_team": "Add a team",
|
||||
"add_webhook_description": "Receive meeting data in real-time when something happens in {{appName}}",
|
||||
@@ -1598,7 +1601,8 @@
|
||||
"attendee_email_variable": "Attendee email",
|
||||
"attendee_email_info": "The person booking's email",
|
||||
"kbar_search_placeholder": "Type a command or search...",
|
||||
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.",
|
||||
"invalid_credential": "It looks like permissions expired or were revoked for {{appName}}.",
|
||||
"invalid_credential_action": "Reinstall app",
|
||||
"reschedule_reason": "Reschedule reason",
|
||||
"choose_common_schedule_team_event": "Choose a common schedule",
|
||||
"choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.",
|
||||
@@ -2195,7 +2199,7 @@
|
||||
"create_your_org_description": "Upgrade to Enterprise and receive a subdomain, unified billing, Insights, extensive whitelabeling and more",
|
||||
"other_payment_app_enabled": "You can only enable one payment app per event type",
|
||||
"admin_delete_organization_description": "<ul><li>Teams that are member of this organization will also be deleted along with their event-types</li><li>Users that were part of the organization will not be deleted and their event-types will also remain intact.</li><li>Usernames would be changed to allow them to exist outside the organization</li></ul>",
|
||||
"admin_delete_organization_title": "Delete organization?",
|
||||
"admin_delete_organization_title": "Delete {{organizationName}}?",
|
||||
"published": "Published",
|
||||
"unpublished": "Unpublished",
|
||||
"publish": "Publish",
|
||||
@@ -2262,5 +2266,13 @@
|
||||
"field_identifiers_as_variables": "Use field identifiers as variables for your custom event redirect",
|
||||
"field_identifiers_as_variables_with_example": "Use field identifiers as variables for your custom event redirect (e.g. {{variable}})",
|
||||
"account_already_linked": "Account is already linked",
|
||||
"routers": "Routers",
|
||||
"primary": "Primary",
|
||||
"make_primary": "Make primary",
|
||||
"add_email": "Add Email",
|
||||
"add_email_description": "Add an email address to replace your primary or to use as an alternative email on your event types.",
|
||||
"confirm_email": "Confirm your email",
|
||||
"confirm_email_description": "We sent an email to <strong>{{email}}</strong>. Click the link in the email to verify this address.",
|
||||
"send_event_details_to": "Send event details to",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -893,7 +893,7 @@
|
||||
"number_apps_other": "{{count}} aplicaciones",
|
||||
"trending_apps": "Aplicaciones populares",
|
||||
"most_popular": "Más popular",
|
||||
"installed_apps": "Aplicaciones instaladas",
|
||||
"installed_apps": "Apps instaladas",
|
||||
"free_to_use_apps": "Gratis",
|
||||
"no_category_apps": "No hay aplicaciones de {{category}}",
|
||||
"all_apps": "Todas las aplicaciones",
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}} : Instructions de réinitialisation de mot de passe",
|
||||
"verify_email_subject": "{{appName}} : Vérifiez votre compte",
|
||||
"check_your_email": "Vérifiez votre e-mail",
|
||||
"old_email_address": "Ancienne adresse e-mail",
|
||||
"new_email_address": "Nouvelle adresse e-mail",
|
||||
"verify_email_page_body": "Nous avons envoyé un e-mail à {{email}}. Il est important de vérifier votre adresse e-mail pour garantir la meilleure délivrabilité des e-mails et des calendriers de {{appName}}.",
|
||||
"verify_email_banner_body": "Vérifiez votre adresse e-mail pour garantir la meilleure délivrabilité des e-mails et des calendriers",
|
||||
"verify_email_email_header": "Vérifiez votre adresse e-mail",
|
||||
"verify_email_email_button": "Vérifier l'adresse e-mail",
|
||||
"verify_email_change_description": "Vous avez récemment demandé à changer l'adresse e-mail que vous utilisez pour vous connecter à votre compte {{appName}}. Veuillez cliquer sur le bouton ci-dessous pour confirmer votre nouvelle adresse e-mail.",
|
||||
"verify_email_change_success_toast": "Mise à jour de votre adresse e-mail. Nouvelle : {{email}}",
|
||||
"verify_email_change_failure_toast": "Échec de la mise à jour de l'adresse e-mail.",
|
||||
"change_of_email": "Faites vérifier votre nouvelle adresse e-mail {{appName}}",
|
||||
"change_of_email_toast": "Nous avons envoyé un lien de vérification à {{email}}. Nous mettrons à jour votre adresse e-mail une fois que vous aurez cliqué sur ce lien.",
|
||||
"copy_somewhere_safe": "Sauvegardez cette clé API dans un endroit sûr. Vous ne pourrez plus la revoir.",
|
||||
"verify_email_email_body": "Veuillez vérifier votre adresse e-mail en cliquant sur le bouton ci-dessous.",
|
||||
"verify_email_by_code_email_body": "Veuillez vérifier votre adresse e-mail en utilisant le code ci-dessous.",
|
||||
@@ -56,6 +63,17 @@
|
||||
"a_refund_failed": "Un remboursement a échoué",
|
||||
"awaiting_payment_subject": "En attente de paiement : {{title}} le {{date}}",
|
||||
"meeting_awaiting_payment": "Votre rendez-vous est en attente de paiement",
|
||||
"dark_theme_contrast_error": "La couleur du thème sombre ne passe pas la vérification du contraste. Nous vous recommandons de modifier cette couleur afin que vos boutons soient plus visibles.",
|
||||
"light_theme_contrast_error": "La couleur du thème clair ne passe pas la vérification du contraste. Nous vous recommandons de modifier cette couleur afin que vos boutons soient plus visibles.",
|
||||
"payment_not_created_error": "Le paiement n'a pas pu être créé",
|
||||
"couldnt_charge_card_error": "Impossible de débiter la carte pour le Paiement",
|
||||
"no_available_users_found_error": "Aucun utilisateur disponible n'a été trouvé. Pourriez-vous essayer un autre créneau horaire ?",
|
||||
"request_body_end_time_internal_error": "Erreur interne. Le corps de la demande ne contient pas l'heure de fin",
|
||||
"create_calendar_event_error": "Impossible de créer un événement de Calendrier dans le calendrier de l'Organisateur",
|
||||
"update_calendar_event_error": "Impossible de mettre à jour l'événement de Calendrier.",
|
||||
"delete_calendar_event_error": "Impossible de supprimer l'événement de Calendrier.",
|
||||
"already_signed_up_for_this_booking_error": "Vous êtes déjà inscrit(e) à cette réservation.",
|
||||
"hosts_unavailable_for_booking": "Certains hôtes sont indisponibles pour la réservation.",
|
||||
"help": "Aide",
|
||||
"price": "Prix",
|
||||
"paid": "Payé",
|
||||
@@ -101,6 +119,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Replanification requise : veuillez réserver un nouveau créneau pour {{eventType}} avec {{name}}",
|
||||
"hi_user_name": "Bonjour {{name}}",
|
||||
"ics_event_title": "{{eventType}} avec {{name}}",
|
||||
"please_book_a_time_sometime_later": "Oups, nous n'avons pas pu vous connecter cette fois-ci. Veuillez plutôt planifier un appel ultérieur",
|
||||
"new_event_subject": "Nouvel événement : {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Rejoindre par {{entryPoint}}",
|
||||
"notes": "Notes",
|
||||
@@ -117,15 +136,20 @@
|
||||
"meeting_id": "ID du rendez-vous",
|
||||
"meeting_password": "Mot de passe du rendez-vous",
|
||||
"meeting_url": "Lien du rendez-vous",
|
||||
"meeting_url_not_found": "L'URL du rendez-vous est introuvable",
|
||||
"token_not_found": "Jeton introuvable",
|
||||
"some_other_host_already_accepted_the_meeting": "D'autres hôtes ont déjà accepté le rendez-vous. Voulez-vous quand même participer ? <1>Aller quand même dans Rendez-vous</1>",
|
||||
"meeting_request_rejected": "Votre demande de rendez-vous a été refusée",
|
||||
"rejected_event_type_with_organizer": "Refusé : {{eventType}} avec {{organizer}} le {{date}}",
|
||||
"hi": "Bonjour",
|
||||
"join_team": "Rejoindre l'équipe",
|
||||
"manage_this_team": "Gérer cette équipe",
|
||||
"team_info": "Informations sur l'équipe",
|
||||
"join_meeting": "Rejoindre le rendez-vous",
|
||||
"request_another_invitation_email": "Si vous préférez ne pas utiliser {{toEmail}} comme votre adresse e-mail {{appName}} ou si vous avez déjà un compte {{appName}}, veuillez demander une autre invitation à cette adresse.",
|
||||
"you_have_been_invited": "Vous avez été invité à rejoindre l'équipe {{teamName}}",
|
||||
"user_invited_you": "{{user}} vous a invité à rejoindre l'{{entity}} {{team}} sur {{appName}}",
|
||||
"user_invited_you_to_subteam": "{{user}} vous a invité(e) à rejoindre l'équipe {{team}} de l'organisation {{parentTeamName}} sur {{appName}}",
|
||||
"hidden_team_member_title": "Vous êtes masqué dans cette équipe",
|
||||
"hidden_team_member_message": "Votre place n'est pas payée, passez au plan Pro ou faites savoir au propriétaire de l'équipe qu'il peut payer votre place.",
|
||||
"hidden_team_owner_message": "Vous avez besoin d'un compte Pro pour utiliser les équipes, vous êtes masqué jusqu'à ce que vous mettiez à niveau.",
|
||||
@@ -285,6 +309,7 @@
|
||||
"welcome_to_calcom": "Bienvenue sur {{appName}}",
|
||||
"welcome_instructions": "Dites-nous comment vous appeler et indiquez-nous dans quel fuseau horaire vous vous trouvez. Vous pourrez modifier cela plus tard. ",
|
||||
"connect_caldav": "Se connecter au serveur CalDAV (Bêta)",
|
||||
"connect_ics_feed": "Connexion à un flux ICS",
|
||||
"connect": "Connecter",
|
||||
"try_for_free": "Essayez gratuitement",
|
||||
"create_booking_link_with_calcom": "Créez votre propre lien de réservation avec {{appName}}",
|
||||
@@ -423,12 +448,14 @@
|
||||
"browse_api_documentation": "Parcourez la documentation de l'API",
|
||||
"leverage_our_api": "Profitez de notre API pour tout personnaliser et contrôler.",
|
||||
"create_webhook": "Créer un webhook",
|
||||
"instant_meeting": "Rendez-vous instantané créé",
|
||||
"booking_cancelled": "Réservation annulée",
|
||||
"booking_rescheduled": "Réservation replanifiée",
|
||||
"recording_ready": "Lien de téléchargement de l'enregistrement prêt",
|
||||
"booking_created": "Réservation créée",
|
||||
"booking_rejected": "Réservation rejetée",
|
||||
"booking_requested": "Réservation demandée",
|
||||
"booking_payment_initiated": "Paiement de la réservation initiée",
|
||||
"meeting_ended": "Rendez-vous terminé",
|
||||
"form_submitted": "Formulaire envoyé",
|
||||
"booking_paid": "Réservation payée",
|
||||
@@ -536,6 +563,7 @@
|
||||
"enter_number_between_range": "Veuillez entrer un nombre entre 1 et {{maxOccurences}}",
|
||||
"email_address": "Adresse e-mail",
|
||||
"enter_valid_email": "Veuillez saisir une adresse e-mail valide",
|
||||
"please_schedule_future_call": " Veuillez programmer un appel ultérieur si nous ne sommes pas disponibles dans {{seconds}} secondes",
|
||||
"location": "Lieu",
|
||||
"address": "Adresse",
|
||||
"enter_address": "Entrer une adresse",
|
||||
@@ -595,6 +623,7 @@
|
||||
"new_member": "Nouveau membre",
|
||||
"invite": "Inviter",
|
||||
"add_team_members": "Ajouter des membres d'équipe",
|
||||
"add_org_members": "Ajouter des membres",
|
||||
"add_team_members_description": "Invitez d'autres personnes à rejoindre votre équipe.",
|
||||
"add_team_member": "Ajouter un membre d'équipe",
|
||||
"invite_new_member": "Inviter un nouveau membre d'équipe",
|
||||
@@ -609,6 +638,7 @@
|
||||
"hide_book_a_team_member_description": "Masquez le bouton Réserver un membre d'équipe de vos pages publiques.",
|
||||
"danger_zone": "Zone de danger",
|
||||
"account_deletion_cannot_be_undone": "Attention, la suppression de compte est irréversible.",
|
||||
"team_deletion_cannot_be_undone": "Attention. La suppression de l'équipe est irréversible",
|
||||
"back": "Retour",
|
||||
"cancel": "Annuler",
|
||||
"cancel_all_remaining": "Annuler tous les événements restants",
|
||||
@@ -672,6 +702,7 @@
|
||||
"add_members": "Ajouter des membres...",
|
||||
"no_assigned_members": "Aucun membre assigné",
|
||||
"assigned_to": "Assigné à",
|
||||
"you_must_be_logged_in_to": "Vous devez être connecté(e) à {{url}}",
|
||||
"start_assigning_members_above": "Commencez à assigner des membres ci-dessus",
|
||||
"locked_fields_admin_description": "Les membres ne pourront pas modifier ceci.",
|
||||
"locked_fields_member_description": "Cette option a été verrouillée par l'administrateur de l'équipe.",
|
||||
@@ -776,6 +807,7 @@
|
||||
"additional_input_description": "Exiger que le planificateur saisisse des champs supplémentaires avant la confirmation de la réservation.",
|
||||
"label": "Libellé",
|
||||
"placeholder": "Texte de substitution",
|
||||
"display_add_to_calendar_organizer": "Afficher l'e-mail « Ajouter au calendrier » en tant qu'organisateur",
|
||||
"type": "Type",
|
||||
"edit": "Modifier",
|
||||
"add_input": "Ajouter un champ",
|
||||
@@ -784,6 +816,9 @@
|
||||
"requires_confirmation_description": "La réservation doit être confirmée manuellement avant d'être poussée vers les intégrations et qu'un e-mail de confirmation soit envoyé.",
|
||||
"recurring_event": "Événement récurrent",
|
||||
"recurring_event_description": "Les personnes peuvent s'inscrire à des événements récurrents.",
|
||||
"cannot_be_used_with_paid_event_types": "À ne pas utiliser avec les types d'événement payants",
|
||||
"warning_payment_instant_meeting_event": "Les Rendez-vous instantanés ne sont pas encore pris en charge par les événements récurrents et les applications de paiement",
|
||||
"warning_instant_meeting_experimental": "Expérimental : Les événements de Rendez-vous instantané sont actuellement expérimentaux.",
|
||||
"starting": "Début",
|
||||
"disable_guests": "Désactiver les invités",
|
||||
"disable_guests_description": "Désactivez l'ajout d'invités supplémentaires lors de la réservation.",
|
||||
@@ -792,6 +827,7 @@
|
||||
"private_link_label": "Lien privé",
|
||||
"private_link_hint": "Votre lien privé sera régénéré après chaque utilisation",
|
||||
"copy_private_link": "Copier le lien privé",
|
||||
"copy_private_link_to_event": "Copier le lien privé de l'événement",
|
||||
"private_link_description": "Générez un lien privé à partager sans révéler votre nom d'utilisateur {{appName}}.",
|
||||
"invitees_can_schedule": "Les invités peuvent réserver",
|
||||
"date_range": "Plage de dates",
|
||||
@@ -866,6 +902,7 @@
|
||||
"toggle_calendars_conflict": "Activez/désactivez les calendriers pour lesquels vous souhaitez vérifier les conflits afin d'éviter les doubles réservations.",
|
||||
"connect_additional_calendar": "Connecter un calendrier supplémentaire",
|
||||
"calendar_updated_successfully": "Calendrier mis à jour avec succès",
|
||||
"check_here": "Vérifier ici",
|
||||
"conferencing": "Visioconférence",
|
||||
"calendar": "Calendrier",
|
||||
"payments": "Paiements",
|
||||
@@ -1038,6 +1075,8 @@
|
||||
"user_impersonation_description": "Autorisez notre équipe d'assistance à se connecter temporairement à votre place pour nous aider à résoudre les problèmes que vous pourriez nous signaler.",
|
||||
"team_impersonation_description": "Autorisez les propriétaires/administrateurs de votre équipe à se connecter temporairement à votre place.",
|
||||
"cal_signup_description": "Gratuit pour les particuliers. Plans Équipes pour les fonctionnalités collaboratives.",
|
||||
"make_org_private": "Rendre l'organisation privée",
|
||||
"make_org_private_description": "Les membres de votre organisation ne pourront pas voir les autres membres de l'organisation lorsque cette option est activée.",
|
||||
"make_team_private": "Rendre l'équipe privée",
|
||||
"make_team_private_description": "Les membres de votre équipe ne pourront pas voir les autres membres de l'équipe si cette option est activée.",
|
||||
"you_cannot_see_team_members": "Vous ne pouvez pas voir tous les membres d'une équipe privée.",
|
||||
@@ -1222,6 +1261,7 @@
|
||||
"organizer_name_variable": "Nom de l'organisateur",
|
||||
"app_upgrade_description": "Pour pouvoir utiliser cette fonctionnalité, vous devez passer à un compte Pro.",
|
||||
"invalid_number": "Numéro de téléphone invalide",
|
||||
"invalid_url_error_message": "URL non valide pour {{label}}. Exemple d'URL : {{sampleUrl}}",
|
||||
"navigate": "Naviguer",
|
||||
"open": "Ouvrir",
|
||||
"close": "Fermer",
|
||||
@@ -1260,6 +1300,7 @@
|
||||
"default_calendar_selected": "Calendrier par défaut",
|
||||
"hide_from_profile": "Masquer du profil",
|
||||
"event_setup_tab_title": "Configuration",
|
||||
"availability_not_found_in_schedule_error": "Aucune disponibilité trouvée dans le planning",
|
||||
"event_limit_tab_title": "Limites",
|
||||
"event_limit_tab_description": "Fréquence de réservation",
|
||||
"event_advanced_tab_description": "Paramètres du calendrier, etc.",
|
||||
@@ -1280,6 +1321,7 @@
|
||||
"do_this": "Effectuer ceci",
|
||||
"turn_off": "Désactiver",
|
||||
"turn_on": "Activer",
|
||||
"cancelled_bookings_cannot_be_rescheduled": "Les réservations annulées ne peuvent pas être reprogrammées",
|
||||
"settings_updated_successfully": "Paramètres mis à jour avec succès",
|
||||
"error_updating_settings": "Erreur lors de la mise à jour des paramètres",
|
||||
"personal_cal_url": "Mon lien {{appName}} personnel",
|
||||
@@ -1296,12 +1338,14 @@
|
||||
"select_calendars": "Sélectionnez les calendriers dont vous souhaitez vérifier les conflits afin d'éviter les doubles réservations.",
|
||||
"check_for_conflicts": "Vérifier les conflits",
|
||||
"view_recordings": "Voir les enregistrements",
|
||||
"check_for_recordings": "Chercher des enregistrements",
|
||||
"adding_events_to": "Ajout d'événements",
|
||||
"follow_system_preferences": "Suivre les préférences système",
|
||||
"custom_brand_colors": "Couleurs de marque personnalisées",
|
||||
"customize_your_brand_colors": "Personnalisez la couleur de votre page de réservation à l'image de votre marque.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Supprimez toutes les références à la marque {{appName}}, par exemple « Propulsé par {{appName}} ».",
|
||||
"instant_meeting_with_title": "Rendez-vous instantané avec {{name}}",
|
||||
"profile_picture": "Photo de profil",
|
||||
"upload": "Télécharger",
|
||||
"add_profile_photo": "Ajouter une photo de profil",
|
||||
@@ -1357,6 +1401,7 @@
|
||||
"event_name_info": "Nom du type d'événement",
|
||||
"event_date_info": "Date de l'événement",
|
||||
"event_time_info": "Heure de début de l'événement",
|
||||
"event_type_not_found": "Type d'événement introuvable",
|
||||
"location_variable": "Lieu",
|
||||
"location_info": "Lieu de l'événement",
|
||||
"additional_notes_variable": "Notes supplémentaires",
|
||||
@@ -1399,6 +1444,7 @@
|
||||
"slot_length": "Durée du créneau",
|
||||
"booking_appearance": "Apparence",
|
||||
"appearance_team_description": "Gérez les paramètres d'apparence de vos réservations d'équipe.",
|
||||
"appearance_org_description": "Gérez les paramètres d'apparence de vos réservations d'organisation",
|
||||
"only_owner_change": "Seul le propriétaire de cette équipe peut apporter des modifications à la réservation de l'équipe ",
|
||||
"team_disable_cal_branding_description": "Supprimez toutes les références à la marque {{appName}}, par exemple « Propulsé par {{appName}} ».",
|
||||
"invited_by_team": "{{teamName}} vous a invité à rejoindre son équipe en tant que {{role}}",
|
||||
@@ -1452,6 +1498,7 @@
|
||||
"password_updated": "Mot de passe mis à jour !",
|
||||
"pending_payment": "Paiement en attente",
|
||||
"pending_invites": "Invitations en attente",
|
||||
"pending_organization_invites": "Invitations de l'organisation en attente",
|
||||
"not_on_cal": "Pas sur {{appName}}",
|
||||
"no_calendar_installed": "Aucun calendrier installé",
|
||||
"no_calendar_installed_description": "Vous n'avez pas encore connecté vos calendriers.",
|
||||
@@ -1463,6 +1510,8 @@
|
||||
"report_app": "Signaler l'application",
|
||||
"limit_booking_frequency": "Limiter la fréquence de réservation",
|
||||
"limit_booking_frequency_description": "Limitez le nombre de réservations possibles pour cet événement.",
|
||||
"limit_booking_only_first_slot": "Limiter la réservation à un seul créneau",
|
||||
"limit_booking_only_first_slot_description": "N'autoriser la réservation que du premier créneau de chaque jour",
|
||||
"limit_total_booking_duration": "Limiter la durée totale de réservation",
|
||||
"limit_total_booking_duration_description": "Limitez la durée totale pendant laquelle cet événement peut être réservé.",
|
||||
"add_limit": "Ajouter une limite",
|
||||
@@ -1535,6 +1584,9 @@
|
||||
"member_already_invited": "Le membre a déjà été invité",
|
||||
"already_in_use_error": "Nom d'utilisateur déjà utilisé",
|
||||
"enter_email_or_username": "Saisissez une adresse e-mail ou un nom d'utilisateur",
|
||||
"enter_email": "Saisissez une adresse e-mail",
|
||||
"enter_emails": "Saisissez des adresses e-mail",
|
||||
"too_many_invites": "Vous ne pouvez inviter qu'un maximum de {{nbUsers}} utilisateurs à la fois.",
|
||||
"team_name_taken": "Ce nom est déjà pris",
|
||||
"must_enter_team_name": "Vous devez saisir un nom d'équipe",
|
||||
"team_url_required": "Vous devez saisir un lien d'équipe",
|
||||
@@ -1614,6 +1666,7 @@
|
||||
"individual": "Particulier",
|
||||
"all_bookings_filter_label": "Toutes les réservations",
|
||||
"all_users_filter_label": "Tous les utilisateurs",
|
||||
"all_event_types_filter_label": "Tous les types d'événement",
|
||||
"your_bookings_filter_label": "Vos réservations",
|
||||
"meeting_url_variable": "Lien du rendez-vous",
|
||||
"meeting_url_info": "Lien de la visioconférence du rendez-vous",
|
||||
@@ -1628,6 +1681,7 @@
|
||||
"date_overrides_mark_all_day_unavailable_other": "Marquer comme indisponible aux dates sélectionnées",
|
||||
"date_overrides_add_btn": "Ajouter la superposition",
|
||||
"date_overrides_update_btn": "Mettre à jour la superposition",
|
||||
"date_successfully_added": "Ajout du remplacement de date réussi",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-copie",
|
||||
"set_as_default": "Définir par défaut",
|
||||
"hide_eventtype_details": "Masquer les détails du type d'événement",
|
||||
@@ -1654,6 +1708,7 @@
|
||||
"minimum_round_robin_hosts_count": "Nombre d'hôtes requis pour participer",
|
||||
"hosts": "Hôtes",
|
||||
"upgrade_to_enable_feature": "Vous devez créer une équipe pour activer cette fonctionnalité. Cliquez pour créer une équipe.",
|
||||
"orgs_upgrade_to_enable_feature": "Vous devez passer sur notre formule entreprise pour activer cette fonctionnalité.",
|
||||
"new_attendee": "Nouveau participant",
|
||||
"awaiting_approval": "En attente de validation",
|
||||
"requires_google_calendar": "Cette application nécessite une connexion à Google Agenda",
|
||||
@@ -1708,9 +1763,11 @@
|
||||
"organizer_timezone": "Fuseau horaire de l'organisateur",
|
||||
"email_user_cta": "Voir l'invitation",
|
||||
"email_no_user_invite_heading_team": "Vous avez été invité à rejoindre une équipe {{appName}}",
|
||||
"email_no_user_invite_heading_subteam": "Vous avez été invité(e) à rejoindre une équipe de l'organisation {{parentTeamName}}",
|
||||
"email_no_user_invite_heading_org": "Vous avez été invité à rejoindre une organisation {{appName}}",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} vous a invité à rejoindre son équipe sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
|
||||
"email_user_invite_subheading_team": "{{invitedBy}} vous a invité à rejoindre son équipe « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
|
||||
"email_user_invite_subheading_subteam": "{{invitedBy}} vous a invité(e) à rejoindre son équipe {{teamName}} dans son organisation {{parentTeamName}} sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre équipe d'organiser des rendez-vous sans échanges d'e-mails.",
|
||||
"email_user_invite_subheading_org": "{{invitedBy}} vous a invité à rejoindre son organisation « {{teamName}} » sur {{appName}}. {{appName}} est le planificateur d'événements qui vous permet à vous et à votre organisation d'organiser des rendez-vous sans échanges d'e-mails.",
|
||||
"email_no_user_invite_steps_intro": "Nous vous guiderons à travers quelques étapes courtes et vous profiterez d'une planification sans stress avec votre {{entity}} en un rien de temps.",
|
||||
"email_no_user_step_one": "Choisissez votre nom d'utilisateur",
|
||||
@@ -1843,6 +1900,7 @@
|
||||
"review_event_type": "Examiner le type d'événement",
|
||||
"looking_for_more_analytics": "Vous cherchez plus d'analyses ?",
|
||||
"looking_for_more_insights": "Vous cherchez plus de statistiques ?",
|
||||
"filters": "Filtres",
|
||||
"add_filter": "Ajouter un filtre",
|
||||
"remove_filters": "Effacer les filtres",
|
||||
"select_user": "Sélectionner un utilisateur",
|
||||
@@ -1935,6 +1993,7 @@
|
||||
"connect_google_workspace": "Connecter Google Workspace",
|
||||
"google_workspace_admin_tooltip": "Vous devez être un administrateur d'espace de travail pour utiliser cette fonctionnalité",
|
||||
"first_event_type_webhook_description": "Créez votre premier webhook pour ce type d'événement",
|
||||
"create_instant_meeting_webhook_description": "Créez votre premier webhook avec « Rendez-vous instantané créé » comme déclencheur pour ce type d'événement",
|
||||
"install_app_on": "Installer l’application sur",
|
||||
"create_for": "Créer pour",
|
||||
"currency": "Devise",
|
||||
@@ -1999,6 +2058,8 @@
|
||||
"add_times_to_your_email": "Sélectionnez quelques créneaux disponibles et intégrez-les dans votre e-mail.",
|
||||
"select_time": "Sélectionner un créneau",
|
||||
"select_date": "Sélectionner une date",
|
||||
"connecting_you_to_someone": "Nous vous mettons en relation avec quelqu'un.",
|
||||
"please_do_not_close_this_tab": "Veuillez ne pas fermer cet onglet",
|
||||
"see_all_available_times": "Voir tous les créneaux disponibles",
|
||||
"org_team_names_example_1": "p. ex. Équipe marketing",
|
||||
"org_team_names_example_2": "p. ex. Équipe de vente",
|
||||
@@ -2016,8 +2077,12 @@
|
||||
"description_requires_booker_email_verification": "Pour assurer la vérification par e-mail du participant avant la planification des événements.",
|
||||
"requires_confirmation_mandatory": "Les messages ne peuvent être envoyés aux participants que lorsque le type d'événement nécessite une confirmation.",
|
||||
"organizations": "Organisations",
|
||||
"upload_cal_video_logo": "Téléverser un logo vidéo Cal",
|
||||
"update_cal_video_logo": "Mettre à jour un logo vidéo Cal",
|
||||
"cal_video_logo_upload_instruction": "Pour assurer que votre logo reste visible sur le fond sombre de la vidéo Cal, veuillez téléverser une image de couleur claire au format PNG ou SVG afin de maintenir la transparence.",
|
||||
"org_admin_other_teams": "Autres équipes",
|
||||
"org_admin_other_teams_description": "Ici, vous pouvez voir les équipes au sein de votre organisation dont vous ne faites pas partie. Vous pouvez vous y ajouter si nécessaire.",
|
||||
"not_part_of_org": "Vous ne faites partie d'aucune organisation",
|
||||
"no_other_teams_found": "Aucune autre équipe trouvée",
|
||||
"no_other_teams_found_description": "Il n'y a pas d'autres équipes dans cette organisation.",
|
||||
"attendee_first_name_variable": "Prénom du participant",
|
||||
@@ -2053,52 +2118,149 @@
|
||||
"org_error_processing": "Une erreur s'est produite lors du traitement de cette organisation",
|
||||
"orgs_page_description": "Une liste de toutes les organisations. Accepter une organisation permettra à tous les utilisateurs avec ce domaine de messagerie de s'inscrire SANS vérification d'e-mail.",
|
||||
"unverified": "Non vérifié",
|
||||
"verified": "Vérifié",
|
||||
"dns_missing": "DNS manquant",
|
||||
"dns_configured": "DNS configuré",
|
||||
"mark_dns_configured": "Marquer comme DNS configuré",
|
||||
"value": "Valeur",
|
||||
"your_organization_updated_sucessfully": "Votre organisation a été mise à jour avec succès",
|
||||
"team_no_event_types": "Cette équipe n'a aucun type d'événement",
|
||||
"seat_options_doesnt_multiple_durations": "L'option par place ne prend pas en charge les durées multiples",
|
||||
"include_calendar_event": "Inclure l'événement du calendrier",
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Ajoutées récemment",
|
||||
"connect_all_calendars": "Connectez vos calendriers",
|
||||
"connect_all_calendars_description": "{{appName}} relève la disponibilité de tous vos calendriers existants.",
|
||||
"workflow_automation": "Automatisation de workflows",
|
||||
"workflow_automation_description": "Personnalisez votre expérience de planification avec les workflows",
|
||||
"scheduling_for_your_team": "Automatisation de workflows",
|
||||
"scheduling_for_your_team_description": "Planifiez pour votre équipe grâce à la planification collective et par permutation circulaire",
|
||||
"no_members_found": "Aucun membre trouvé",
|
||||
"event_setup_length_error": "Configuration de l'événement : la durée doit être d'au moins 1 minute.",
|
||||
"availability_schedules": "Horaires de disponibilité",
|
||||
"unauthorized": "Non autorisé",
|
||||
"access_cal_account": "{{clientName}} aimerait accéder à votre compte {{appName}}",
|
||||
"select_account_team": "Sélectionner un compte ou une équipe",
|
||||
"allow_client_to": "Cela permettra au {{clientName}} de",
|
||||
"associate_with_cal_account": "Vous associer à vos informations personnelles de {{clientName}}",
|
||||
"see_personal_info": "Voir vos informations personnelles, y compris celles que vous avez rendues publiques",
|
||||
"see_primary_email_address": "Voir votre adresse e-mail principale",
|
||||
"connect_installed_apps": "Vous connecter à vos applications installées",
|
||||
"access_event_type": "Lire, modifier, supprimer vos types d'événements",
|
||||
"access_availability": "Lire, modifier, supprimer vos disponibilités",
|
||||
"access_bookings": "Lire, modifier, supprimer vos réservations",
|
||||
"allow_client_to_do": "Autoriser {{clientName}} à faire cela ?",
|
||||
"oauth_access_information": "En cliquant sur autoriser, vous autorisez cette application à utiliser vos informations conformément à ses conditions de service et à sa politique de confidentialité. Vous pouvez supprimer l'accès dans l'App Store de {{appName}}.",
|
||||
"allow": "Autoriser",
|
||||
"view_only_edit_availability_not_onboarded": "Cet utilisateur n'a pas terminé son intégration. Vous ne pourrez pas définir sa disponibilité tant qu'il n'aura pas terminé son intégration.",
|
||||
"view_only_edit_availability": "Vous consultez les disponibilités de cet utilisateur. Vous ne pouvez modifier que vos propres disponibilités.",
|
||||
"you_can_override_calendar_in_advanced_tab": "Vous pouvez modifier ceci pour chaque événement dans les paramètres avancés de chaque type d'événement.",
|
||||
"edit_users_availability": "Modifier la disponibilité de l'utilisateur : {{username}}",
|
||||
"resend_invitation": "Renvoyer l'invitation",
|
||||
"invitation_resent": "L'invitation a été renvoyée.",
|
||||
"saml_sso": "SAML",
|
||||
"add_client": "Ajouter un client",
|
||||
"copy_client_secret_info": "Après avoir copié le secret, vous ne pourrez plus le voir",
|
||||
"add_new_client": "Ajouter un nouveau client",
|
||||
"this_app_is_not_setup_already": "Cette application n'a pas encore été configurée",
|
||||
"as_csv": "au format CSV",
|
||||
"overlay_my_calendar": "Superposer mon calendrier",
|
||||
"overlay_my_calendar_toc": "En vous connectant à votre calendrier, vous acceptez notre politique de confidentialité et nos conditions d'utilisation. Vous pouvez révoquer cet accès à tout moment.",
|
||||
"view_overlay_calendar_events": "Consultez les événements de votre calendrier afin d'éviter les réservations incompatibles.",
|
||||
"join_event_location": "Rejoignez {{eventLocationType}}",
|
||||
"troubleshooting": "Dépannage",
|
||||
"calendars_were_checking_for_conflicts": "Calendriers dont nous vérifions les conflits",
|
||||
"availabilty_schedules": "Horaires de disponibilité",
|
||||
"manage_calendars": "Gérer les calendriers",
|
||||
"manage_availability_schedules": "Gérer les horaires de disponibilité",
|
||||
"lock_timezone_toggle_on_booking_page": "Verrouiller le fuseau horaire sur la page de réservation",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Pour verrouiller le fuseau horaire sur la page de réservation, utile pour les événements en personne.",
|
||||
"event_setup_multiple_payment_apps_error": "Vous ne pouvez activer qu'une seule application de paiement par type d'événement.",
|
||||
"number_in_international_format": "Veuillez entrer le numéro au format international.",
|
||||
"install_calendar": "Installer Calendrier",
|
||||
"branded_subdomain": "Sous-domaine de marque",
|
||||
"branded_subdomain_description": "Obtenez votre propre sous-domaine de marque, tel que acme.cal.com",
|
||||
"org_insights": "Statistiques à l'échelle de l'organisation",
|
||||
"org_insights_description": "Comprenez comment l'ensemble de votre organisation passe son temps",
|
||||
"extensive_whitelabeling": "Marque blanche étendue",
|
||||
"extensive_whitelabeling_description": "Personnalisez votre expérience de planification avec votre propre logo, vos couleurs, etc.",
|
||||
"unlimited_teams": "Équipes illimitées",
|
||||
"unlimited_teams_description": "Ajoutez autant de sous-équipes que nécessaire à votre organisation",
|
||||
"unified_billing": "Facturation unifiée",
|
||||
"unified_billing_description": "Ajoutez une seule carte de crédit pour payer tous les abonnements de votre équipe",
|
||||
"advanced_managed_events": "Types d'événements gérés avancés",
|
||||
"advanced_managed_events_description": "Ajoutez une seule carte de crédit pour payer tous les abonnements de votre équipe",
|
||||
"enterprise_description": "Passez en Entreprise pour créer votre organisation",
|
||||
"create_your_org": "Créer votre organisation",
|
||||
"create_your_org_description": "Passez en Entreprise et recevez un sous-domaine, une facturation unifiée, des statistiques, un marquage blanc extensif et plus encore",
|
||||
"other_payment_app_enabled": "Vous ne pouvez activer qu'une seule application de paiement par type d'événement",
|
||||
"admin_delete_organization_description": "<ul><li>Les équipes qui sont membres de cette organisation seront également supprimées, ainsi que leurs types d'événements</li><li>Les utilisateurs qui faisaient partie de l'organisation ne seront pas supprimés et leurs types d'événements resteront intacts.</li><li>Les noms d'utilisateur seront modifiés pour leur permettre d'exister en dehors de l'organisation</li></ul>",
|
||||
"admin_delete_organization_title": "Supprimer {{organizationName}} ?",
|
||||
"published": "Publiée",
|
||||
"unpublished": "Non publiée",
|
||||
"publish": "Publier",
|
||||
"org_publish_error": "L'organisation n'a pas pu être publiée",
|
||||
"troubleshooter_tooltip": "Ouvrez l'outil de dépannage et déterminez ce qui ne va pas avec votre planning",
|
||||
"need_help": "Besoin d'aide ?",
|
||||
"troubleshooter": "Dépannage",
|
||||
"please_install_a_calendar": "Veuillez installer un publier",
|
||||
"instant_tab_title": "Réservation instantanée",
|
||||
"instant_event_tab_description": "Autoriser les gens à réserver immédiatement",
|
||||
"uprade_to_create_instant_bookings": "Passez en Entreprise et autorisez les invités à rejoindre un appel instantané auquel les participants peuvent accéder directement. Ceci n'est valable que pour les types d'événements d'équipe",
|
||||
"dont_want_to_wait": "Vous ne voulez pas attendre ?",
|
||||
"meeting_started": "Le rendez-vous a commencé",
|
||||
"pay_and_book": "Payer pour réserver",
|
||||
"booking_not_found_error": "Impossible de trouver la réservation",
|
||||
"booking_seats_full_error": "Les places de réservation sont complètes",
|
||||
"missing_payment_credential_error": "Identifiants de paiement manquants",
|
||||
"missing_payment_app_id_error": "Id de l'application de paiement manquant",
|
||||
"not_enough_available_seats_error": "La réservation n'a pas assez de places disponibles",
|
||||
"user_redirect_title": "{{username}} est actuellement en absence de courte durée.",
|
||||
"user_redirect_description": "Pendant ce temps, {{profile.username}} sera responsable de tous les rendez-vous programmés de la part de {{username}}.",
|
||||
"out_of_office": "Pas au bureau",
|
||||
"out_of_office_description": "Configurez des actions dans votre profil pendant votre absence.",
|
||||
"send_request": "Envoyer une demande",
|
||||
"start_date_and_end_date_required": "Les dates de début et de fin sont requises",
|
||||
"start_date_must_be_before_end_date": "La date de début doit être antérieure à la date de fin",
|
||||
"start_date_must_be_in_the_future": "La date de début doit être dans le futur",
|
||||
"user_not_found": "Utilisateur introuvable",
|
||||
"out_of_office_entry_already_exists": "L'entrée Pas au bureau existe déjà",
|
||||
"out_of_office_id_required": "L'id de l'entrée Pas au bureau est requis",
|
||||
"booking_redirect_infinite_not_allowed": "Une redirection de réservation de cet utilisateur vers vous existe déjà.",
|
||||
"success_entry_created": "Création d'une nouvelle entrée réussie",
|
||||
"booking_redirect_email_subject": "Notification de redirection de réservation",
|
||||
"booking_redirect_email_title": "Notification de redirection de réservation",
|
||||
"booking_redirect_email_description": "Vous avez reçu une redirection de réservation de {{toName}} pour que les liens vers son profil soient redirigés vers vous pendant l'intervalle de temps : ",
|
||||
"success_accept_booking_redirect": "Vous avez accepté cette demande de redirection de réservation.",
|
||||
"success_reject_booking_redirect": "Vous avez refusé cette demande de redirection de réservation.",
|
||||
"copy_link_booking_redirect_request": "Copier le lien pour partager la demande",
|
||||
"booking_redirect_request_title": "Demande de redirection de réservation",
|
||||
"select_team_member": "Sélectionner un membre de l'équipe",
|
||||
"going_away_title": "Vous partez ? Marquez simplement le lien de votre profil comme étant indisponible pendant un certain temps.",
|
||||
"redirect_team_enabled": "Redirigez votre profil vers un autre membre de l'équipe",
|
||||
"redirect_team_disabled": "Redirigez votre profil vers un autre membre de l'équipe (formule Équipe requise)",
|
||||
"out_of_office_unavailable_list": "Liste des indisponibilités Pas au bureau",
|
||||
"success_deleted_entry_out_of_office": "Suppression de l'entrée réussie",
|
||||
"temporarily_out_of_office": "Temporairement Pas au bureau ?",
|
||||
"add_a_redirect": "Ajouter une redirection",
|
||||
"create_entry": "Créer une entrée",
|
||||
"time_range": "Plage temporelle",
|
||||
"automatically_add_all_team_members": "Ajouter tous les membres de l'équipe, y compris les futurs membres",
|
||||
"redirect_to": "Redirection vers",
|
||||
"having_trouble_finding_time": "Vous avez du mal à trouver une heure ?",
|
||||
"show_more": "En voir plus",
|
||||
"assignment_description": "Planifiez des rendez-vous lorsque tout le monde est disponible ou faites tourner les membres de votre équipe",
|
||||
"lowest": "le plus bas",
|
||||
"low": "bas",
|
||||
"medium": "moyen",
|
||||
"high": "élevé",
|
||||
"Highest": "le plus élevé",
|
||||
"send_booker_to": "Envoyer un utilisateur vers",
|
||||
"set_priority": "Définir la priorité",
|
||||
"priority_for_user": "Priorité pour {{userName}}",
|
||||
"change_priority": "changer la priorité",
|
||||
"field_identifiers_as_variables": "Utiliser les identifiants de champs comme variables pour la redirection de votre événement personnalisé",
|
||||
"field_identifiers_as_variables_with_example": "Utiliser les identifiants de champs comme variables pour la redirection de votre événement personnalisé (par ex. : {{variable}})",
|
||||
"account_already_linked": "Le compte est déjà associé",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"identity_provider": "ספק זהויות",
|
||||
"trial_days_left": "נותרו לך $t(day, {\"count\": {{days}} }) לשימוש בגרסת הניסיון של רישוי PRO",
|
||||
"trial_days_left": "יש לך עוד $t(day, {\"count\": {{days}} }) להתנסות בגרסת הפרו",
|
||||
"day_one": "יום {{count}}",
|
||||
"day_other": "{{count}} ימים",
|
||||
"second_one": "שנייה {{count}}",
|
||||
@@ -8,11 +8,11 @@
|
||||
"upgrade_now": "לשדרג עכשיו",
|
||||
"accept_invitation": "אישור הזמנה",
|
||||
"calcom_explained": "{{appName}} מספקת תשתית ליצירת לוחות זמנים שתתאים לכולם.",
|
||||
"calcom_explained_new_user": "סיים להגדיר את החשבון של {{appName}}! אתה רק מספר צעדים מפתרון כל בעיות התזמון שלך.",
|
||||
"calcom_explained_new_user": "נא לסיים להגדיר את החשבון שלך ב־{{appName}}! בעוד מספר צעדים כל בעיות התזמון שלך ייפתרו.",
|
||||
"have_any_questions": "יש לך שאלות? אנחנו כאן כדי לעזור.",
|
||||
"reset_password_subject": "{{appName}}: הנחיות לאיפוס סיסמה",
|
||||
"verify_email_subject": "{{appName}}: יש לאמת את החשבון",
|
||||
"check_your_email": "בדוק/בדקי את הדוא״ל שלך",
|
||||
"check_your_email": "נא לבדוק את הדוא״ל שלך",
|
||||
"old_email_address": "כתובת דוא״ל ישנה",
|
||||
"new_email_address": "כתובת דוא״ל חדשה",
|
||||
"verify_email_page_body": "שלחנו הודעת דוא״ל לכתובת {{email}}. חשוב שתאמת/י את כתובת הדוא״ל שלך כדי להבטיח שתוכל/י לקבל הודעות מ-{{appName}} לכתובת הדוא״ל וללוח השנה שלך.",
|
||||
@@ -29,20 +29,20 @@
|
||||
"verify_email_by_code_email_body": "נא לאמת את כתובת הדוא״ל באמצעות הקוד שמתחת.",
|
||||
"verify_email_email_link_text": "הנה הקישור, למקרה שאינך אוהב/ת ללחוץ על כפתורים:",
|
||||
"email_verification_code": "הזנת קוד האימות",
|
||||
"email_verification_code_placeholder": "יש להזין את קוד האימות שנשלח אליך בדוא\"ל",
|
||||
"email_verification_code_placeholder": "נא למלא את קוד האימות שנשלח אליך בדוא״ל",
|
||||
"incorrect_email_verification_code": "קוד האימות שגוי.",
|
||||
"email_sent": "הודעת הדוא״ל נשלחה בהצלחה",
|
||||
"email_not_sent": "אירעה שגיאה בעת שליחת הדוא\"ל",
|
||||
"event_declined_subject": "נדחה: {{title}} ב- {{date}}",
|
||||
"event_cancelled_subject": "בוטל: {{title}} ב-{{date}}",
|
||||
"email_not_sent": "אירעה שגיאה בשליחת הדוא״ל",
|
||||
"event_declined_subject": "נדחה: {{title}} ב־{{date}}",
|
||||
"event_cancelled_subject": "בוטל: {{title}} ב־{{date}}",
|
||||
"event_request_declined": "בקשת האירוע שלך נדחתה",
|
||||
"event_request_declined_recurring": "בקשת האירוע החוזר שלך נדחתה",
|
||||
"event_request_cancelled": "האירוע המתוכנן שלך בוטל",
|
||||
"organizer": "מארגן",
|
||||
"organizer": "ארגון",
|
||||
"need_to_reschedule_or_cancel": "רוצה לקבוע מועד חדש או לבטל?",
|
||||
"no_options_available": "אין אפשרויות זמינות",
|
||||
"cancellation_reason": "הסיבה לביטול (לא חובה)",
|
||||
"cancellation_reason_placeholder": "מדוע בחרת לבטל? (לא חובה)",
|
||||
"cancellation_reason": "הסיבה לביטול (רשות)",
|
||||
"cancellation_reason_placeholder": "מדוע בחרת לבטל?",
|
||||
"rejection_reason": "הסיבה לדחייה",
|
||||
"rejection_reason_title": "לדחות את בקשת ההזמנה?",
|
||||
"rejection_reason_description": "בטוח שברצונך לדחות את ההזמנה? אנחנו ניידע את האדם שניסה להזמין. יש לך אפשרות להוסיף את הסיבה לדחייה למטה.",
|
||||
@@ -50,7 +50,7 @@
|
||||
"manage_this_event": "נהל את האירוע הזה",
|
||||
"invite_team_member": "הזמן חבר צוות",
|
||||
"invite_team_individual_segment": "הזמנת אדם אחד",
|
||||
"invite_team_bulk_segment": "ייבוא בכמות גדולה",
|
||||
"invite_team_bulk_segment": "ייבוא במרוכז",
|
||||
"invite_team_notifcation_badge": "הזמנה",
|
||||
"your_event_has_been_scheduled": "נקבע מועד לאירוע שלך",
|
||||
"your_event_has_been_scheduled_recurring": "נקבע מועד לאירוע החוזר שלך",
|
||||
@@ -86,7 +86,7 @@
|
||||
"acquire_license": "כדי להסיר את התנאים האלה, ניתן לקנות רישיון מסחרי על ידי שליחת דוא\"ל",
|
||||
"terms_summary": "סיכום התנאים",
|
||||
"open_env": "יש לפתוח את .env ולאשר את הרישיון שלנו",
|
||||
"env_changed": "שיניתי את ה-.env שלי",
|
||||
"env_changed": "שיניתי את ה־.env שלי",
|
||||
"accept_license": "אישור הרישיון",
|
||||
"still_waiting_for_approval": "אירוע עדיין ממתין לאישור",
|
||||
"event_is_still_waiting": "בקשת האירוע עדיין בהמתנה: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
@@ -104,7 +104,7 @@
|
||||
"someone_requested_password_reset": "מישהו ביקש קישור כדי לשנות את הסיסמה שלך.",
|
||||
"password_reset_email_sent": "אם כתובת הדוא\"ל הזו קיימת במערכת שלנו, אמור להישלח אליך דוא\"ל לאיפוס.",
|
||||
"password_reset_instructions": "אם הבקשה לא היתה שלך, ניתן להתעלם מהודעת הדוא\"ל הזו בבטחה, והסיסמה שלך לא תשתנה.",
|
||||
"event_awaiting_approval_subject": "ממתין לאישור: {{title}} ב- {{date}}",
|
||||
"event_awaiting_approval_subject": "ממתין לאישור: {{title}} ב־{{date}}",
|
||||
"event_still_awaiting_approval": "אירוע עדיין ממתין לאישורך",
|
||||
"booking_submitted_subject": "ההזמנה נשלחה: {{title}} ב- {{date}}",
|
||||
"download_recording_subject": "הורדת ההקלטה: {{title}} בתאריך {{date}}",
|
||||
@@ -807,6 +807,8 @@
|
||||
"additional_input_description": "דרוש מהמתזמן להזין נתונים נוספים לפני אישור התזמון",
|
||||
"label": "תווית",
|
||||
"placeholder": "מציין מיקום",
|
||||
"display_add_to_calendar_organizer": "הצגת דוא״ל ‚הוספה ללוח השנה’ כגוף מארגן",
|
||||
"reconnect_calendar_to_use": "נא לשים לב שכנראה צריך להתנתק ואז להתחבר ללוח השנה שלך מחדש כדי להשתמש ביכולת הזאת.",
|
||||
"type": "סוג",
|
||||
"edit": "עריכה",
|
||||
"add_input": "הוספת קלט",
|
||||
@@ -1473,6 +1475,8 @@
|
||||
"copy_link_to_form": "להעתיק את הקישור לטופס",
|
||||
"theme": "ערכת נושא",
|
||||
"theme_applies_note": "חלה רק על דפים של הזמנות ציבוריות",
|
||||
"app_theme": "ערכת עיצוב ללוח בקרה",
|
||||
"app_theme_applies_note": "חל רק על לוח הבקרה שנכנסת אליו",
|
||||
"theme_system": "ברירת המחדל של המערכת",
|
||||
"add_a_team": "הוספת צוות",
|
||||
"add_webhook_description": "לקבל נתוני פגישה בזמן אמת כשמשהו קורה ב-{{appName}}",
|
||||
@@ -1598,6 +1602,7 @@
|
||||
"attendee_email_info": "כתובת הדוא\"ל של האדם שביצע את ההזמנה",
|
||||
"kbar_search_placeholder": "הקלד/י פקודה או חפש/י...",
|
||||
"invalid_credential": "נראה שתוקף ההרשאה פג או שההרשאה בוטלה. יש להתקין מחדש שוב.",
|
||||
"invalid_credential_action": "להתקין את היישום מחדש",
|
||||
"reschedule_reason": "הסיבה לקביעת מועד חדש",
|
||||
"choose_common_schedule_team_event": "בחירת לוח זמנים משותף",
|
||||
"choose_common_schedule_team_event_description": "יש להפעיל אפשרות זו אם רוצים להשתמש בלוח זמנים משותף בין המארחים. אם האפשרות הזו מבוטלת, כל מארח יוזמן לפי לוח הזמנים המשמש כברירת המחדל שלו.",
|
||||
@@ -2261,5 +2266,12 @@
|
||||
"field_identifiers_as_variables": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית",
|
||||
"field_identifiers_as_variables_with_example": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית (למשל: {{variable}})",
|
||||
"account_already_linked": "החשבון כבר מקושר",
|
||||
"primary": "עיקרי",
|
||||
"make_primary": "הפיכה לעיקרי",
|
||||
"add_email": "הוספת כתובת דוא״ל",
|
||||
"add_email_description": "אפשר להוסיף כתובת דוא״ל כדי להחליף את הכתובת העיקרית שלך או ככתובת חלופית לסוגי האירועים שלך.",
|
||||
"confirm_email": "אישור כתובת הדוא״ל שלך",
|
||||
"confirm_email_description": "שלחנו הודעה בדוא״ל אל <strong>{{email}}</strong>. נא ללחוץ על הקישור בהודעה כדי לאמת את הכתובת הזאת.",
|
||||
"send_event_details_to": "שליחת פרטי האירוע אל",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"identity_provider": "Azonosítás szolgáltató",
|
||||
"trial_days_left": "Még $t(day, {\"count\": {{days}} }) maradt a PRO próbaidőszakodból",
|
||||
"day_one": "{{count}} nap",
|
||||
"day_other": "{{count}} nap",
|
||||
@@ -6,20 +7,76 @@
|
||||
"second_other": "{{count}} másodperc",
|
||||
"upgrade_now": "Frissíts most",
|
||||
"accept_invitation": "Meghívó elfogadása",
|
||||
"calcom_explained": "{{appName}} időpont-egyeztető infrastruktúrát biztosít mindenki számára.",
|
||||
"calcom_explained_new_user": "Fejezd be a {{appName}} fiókod beállítását! Már csak pár lépésre vagy attól, hogy megoldd minden időpont-egyeztetési problémád.",
|
||||
"have_any_questions": "Van kérdésed? Itt vagyunk hogy segítsünk.",
|
||||
"reset_password_subject": "{{appName}}: Jelszóváltoztatási lépések",
|
||||
"verify_email_subject": "{{appName}}: Igazold a fiókod",
|
||||
"check_your_email": "Ellenőrizd az email fiókod",
|
||||
"old_email_address": "Régi Email",
|
||||
"new_email_address": "Új Email",
|
||||
"verify_email_page_body": "Küldtünk egy emailt ide: {{email}}. Fontos, hogy igazold vissza az email címed, hogy {{appName}} biztosíthassa Neked a legjobb email és naptár funkciókat.",
|
||||
"verify_email_banner_body": "Igazold vissza az email címed, hogy biztosíthassuk Neked a legjobb email és naptár funkciókat",
|
||||
"verify_email_email_header": "Igazold vissza az email címed",
|
||||
"verify_email_email_button": "Email igazolása",
|
||||
"verify_email_change_success_toast": "Emailed módosítva a következőre: {{email}}",
|
||||
"verify_email_change_failure_toast": "Sikertelen email módosítás.",
|
||||
"change_of_email": "Igazold az új {{appName}} email címed",
|
||||
"change_of_email_toast": "Küldtünk egy visszaigazoló linket ide: {{email}}. Módosítjuk az email címed amint a linkre kattintottál.",
|
||||
"verify_email_email_body": "Kérlek igazold az email címed az alábbi gombra kattintással.",
|
||||
"verify_email_by_code_email_body": "Kérlek igazold az email címed az alábbi kód használatával.",
|
||||
"email_sent": "Email sikeresen elküldve",
|
||||
"event_declined_subject": "Visszautasítva: {{title}}, {{date}}",
|
||||
"event_cancelled_subject": "Lemondva: {{title}} ekkor: {{date}}",
|
||||
"event_request_declined": "Az esemény kérésed vissza lett utasítva",
|
||||
"event_request_declined_recurring": "Az ismétlődő esemény kérésed vissza lett utasítva",
|
||||
"event_request_cancelled": "A lefoglalt időpontod vissza lett mondva",
|
||||
"organizer": "Szervező",
|
||||
"need_to_reschedule_or_cancel": "Átütemeznéd vagy lemondanád?",
|
||||
"cancellation_reason": "Lemondás oka (opcionális)",
|
||||
"cancellation_reason_placeholder": "Mi az oka, hogy le kell mondanod az időpontot?",
|
||||
"rejection_reason": "Lemondás oka",
|
||||
"rejection_reason_title": "Visszamondod a foglalási kérelmet?",
|
||||
"rejection_reason_description": "Biztosan vissza szeretnéd utasítani a foglalást? Értesítjük azt aki próbált foglalni. Lejjebb megadhatsz egy okot.",
|
||||
"rejection_confirmation": "Foglalás visszautasítása",
|
||||
"your_event_has_been_scheduled": "Az időpontod le lett foglalva",
|
||||
"your_event_has_been_scheduled_recurring": "Az ismétlődő időpontod le lett foglalva",
|
||||
"refund_failed_subject": "Sikertelen visszatérítés: {{name}} - {{date}} - {{eventType}}",
|
||||
"refund_failed": "Sikertelen visszatérítés ezért: {{eventType}}, {{userName}}, {{date}}.",
|
||||
"a_refund_failed": "Sikertelen visszatérítés",
|
||||
"awaiting_payment_subject": "Fizetésre vár: {{title}}, {{date}}",
|
||||
"already_signed_up_for_this_booking_error": "Már jelentkeztél erre a foglalásra.",
|
||||
"hosts_unavailable_for_booking": "Valaki a szervezők közül nem elérhető foglalásra.",
|
||||
"help": "Segítség",
|
||||
"price": "Ár",
|
||||
"paid": "Fizetett",
|
||||
"refunded": "Visszatérítve",
|
||||
"missing_card_fields": "Hiányzó kártya adatok",
|
||||
"pay_now": "Fizetés most",
|
||||
"accept_license": "Licensz elfogadása",
|
||||
"event_is_still_waiting": "Még mindig függőben lévő kérés: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"confirmed_event_type_subject": "Megerősítve: {{eventType}}, {{name}} ekkor: {{date}}",
|
||||
"new_event_request": "Új időpontkérés: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"check_bookings_page_to_confirm_or_reject": "Nézd meg a foglalási oldalad hogy jóváhagyd vagy visszautasítsd a foglalást.",
|
||||
"event_awaiting_approval_subject": "Jóváhagyásra vár: {{title}}, {{date}}",
|
||||
"booking_submitted_subject": "Foglalás elküldve: {{title}}, {{date}}",
|
||||
"download_recording_subject": "Felvétel letöltése: {{title}}, {{date}}",
|
||||
"your_meeting_has_been_booked": "A találkozód le lett foglalva",
|
||||
"event_type_has_been_rescheduled_on_time_date": "{{title}} át lett ütemezve ekkorra: {{date}}.",
|
||||
"event_has_been_rescheduled": "Frissítve - Az időpontod át lett ütemezve",
|
||||
"request_reschedule_subtitle": "{{organizer}} lemondta a foglalást és kérte, hogy válassz egy másik időpontot.",
|
||||
"request_reschedule_title_organizer": "{{attendee}} időpontmódosítást kér",
|
||||
"request_reschedule_subtitle_organizer": "Lemondtad a foglalást és {{attendee}} választhat új időpontot veled.",
|
||||
"rescheduled_event_type_subject": "Átütemezési kérés kikülve: {{eventType}}, {{name}}, {{date}}",
|
||||
"requested_to_reschedule_subject_attendee": "Átütmezés szükséges: Kérlek foglalj új időpontot a következőre: {{eventType}}, {{name}}",
|
||||
"hi_user_name": "Szia {{name}}",
|
||||
"new_event_subject": "Új esemény: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"notes": "Jegyzetek",
|
||||
"need_to_make_a_change": "Változtatnál valamit?",
|
||||
"meeting_id": "Megbeszélés azonosítója",
|
||||
"meeting_password": "Megbeszélés jelszava",
|
||||
"meeting_url": "Megbeszélés URL címe",
|
||||
"rejected_event_type_with_organizer": "Visszautasítva: {{eventType}}, {{organizer}}, {{date}}",
|
||||
"hi": "Szia",
|
||||
"hey_there": "Üdv itt,",
|
||||
"confirm_delete_webhook": "Igen, webhook törlése",
|
||||
@@ -53,6 +110,7 @@
|
||||
"documentation": "Dokumentáció",
|
||||
"blog": "Blog",
|
||||
"404_page_not_found": "404: Az oldal nem található.",
|
||||
"booker_event_not_found": "Nem találjuk az eseményt amire szeretnél foglalni.",
|
||||
"15min_meeting": "15 perces megbeszélés",
|
||||
"30min_meeting": "30 perces megbeszélés",
|
||||
"secret": "Titok",
|
||||
@@ -67,6 +125,8 @@
|
||||
"done": "Kész",
|
||||
"all": "Minden alkalmazás",
|
||||
"finish": "Befelyezés",
|
||||
"continue_without_calendar": "Folyatatás naptár nélkül",
|
||||
"continue_with": "Folyatatás {{appName}}-val",
|
||||
"current_time": "Jelenlegi idő",
|
||||
"welcome": "Üdvözöljük",
|
||||
"connect_caldav": "Csatlakozás a CalDav szerverhez",
|
||||
@@ -79,9 +139,29 @@
|
||||
"where": "Hol",
|
||||
"add_to_calendar": "Hozzáadás naptárhoz",
|
||||
"other": "Egyéb",
|
||||
"email_sign_in_subject": "Bejelentkezési link a következőhöz: {{appName}}",
|
||||
"emailed_you_and_attendees": "Mindenkinek küldtünk egy emailt a naptár meghívóval és a részletekkel.",
|
||||
"emailed_you_and_attendees_recurring": "Mindenkinek küldtünk emailben egy naptár meghívót a részletekkel az első ismétlődő eseményről.",
|
||||
"emailed_you_and_any_other_attendees": "Küldtünk egy email mindenkinek ezekről az információkról.",
|
||||
"user_needs_to_confirm_or_reject_booking": "{{user}} még mindig vissza kell igazolni vagy elutasítania a foglalást.",
|
||||
"user_needs_to_confirm_or_reject_booking_recurring": "{{user}} résztvevőnek még mindig vissza kell igazolnia vagy elutasítania a foglalást az ismétlődő találkozóra.",
|
||||
"meeting_is_scheduled": "Találkozó sikeresen lefoglalva",
|
||||
"booking_submitted": "A foglalásod be lett küldve",
|
||||
"booking_confirmed": "A foglalásod vissza lett igazolva",
|
||||
"try_again": "Próbáld újra",
|
||||
"login": "Bejelentkezés",
|
||||
"success": "Siker",
|
||||
"back_to_bookings": "Vissza a foglalásokhoz",
|
||||
"cancelled": "Lemondva",
|
||||
"cancellation_successful": "Sikeres lemondás",
|
||||
"really_cancel_booking": "Biztosan lemondod a foglalásod?",
|
||||
"cannot_cancel_booking": "Ezt a foglalást nem tudod lemondani",
|
||||
"reschedule_instead": "Helyette át is ütemezheted.",
|
||||
"cancelling_event_recurring": "Ez az esemény része egy ismétlődő eseménynek.",
|
||||
"cancelling_all_recurring": "Ez az összes hátralévő esemény az ismétlőd eseményben.",
|
||||
"booking_already_cancelled": "Ez a foglalás már le lett mondva",
|
||||
"event_between_users": "{{eventName}} {{host}} és {{attendeeName}} közt",
|
||||
"cancelled_bookings": "A lemondott foglalásaid itt fognak megjelenni.",
|
||||
"and": "és",
|
||||
"light": "Világos",
|
||||
"dark": "Sötét",
|
||||
@@ -89,6 +169,13 @@
|
||||
"email_placeholder": "pista@pelda.hu",
|
||||
"full_name": "Teljes név",
|
||||
"create_webhook": "Webhook létrehozása",
|
||||
"booking_cancelled": "Foglalás lemondva",
|
||||
"booking_rescheduled": "Foglalás átütemezve",
|
||||
"booking_created": "Foglalás létrehozva",
|
||||
"booking_rejected": "Foglalás visszautasítva",
|
||||
"booking_requested": "Foglalás kérvényezve",
|
||||
"booking_payment_initiated": "Foglalási fizetés elindítva",
|
||||
"booking_paid": "Foglalás kifizetve",
|
||||
"new_webhook": "Új Webhook",
|
||||
"loading": "Betöltés...",
|
||||
"deleting": "Törlés...",
|
||||
@@ -124,10 +211,19 @@
|
||||
"your_name": "Neved",
|
||||
"email_address": "Email cím",
|
||||
"location": "Helyszín",
|
||||
"in_person_attendee_address": "Személyesen (Résztvevő címén)",
|
||||
"yes": "Igen",
|
||||
"no": "Nem",
|
||||
"additional_notes": "Egyéb jegyzetek",
|
||||
"booking_fail": "Találkozó nem foglalható.",
|
||||
"reschedule_fail": "Nem sikerült időpontot módisítani.",
|
||||
"share_additional_notes": "Kérlek ossz meg velünk bármit, ami segíthet felkészülni a találkozóra.",
|
||||
"booking_reschedule_confirmation": "Módosítsd {{eventTypeTitle}} időpontod nála: {{profileName}}",
|
||||
"in_person_meeting": "Személyes találkozó",
|
||||
"in_person": "Személyesen (Szervező címén)",
|
||||
"phone_number": "Telefonszám",
|
||||
"reschedule": "Átütmezés",
|
||||
"reschedule_this": "Inkább időpontmódosítás",
|
||||
"or": "VAGY",
|
||||
"go_back": "Vissza",
|
||||
"email_or_username": "Email vagy felhasználónév",
|
||||
@@ -136,8 +232,11 @@
|
||||
"danger_zone": "Veszélyzóna",
|
||||
"back": "Vissza",
|
||||
"cancel": "Mégse",
|
||||
"cancel_all_remaining": "Minden hátralévő lemondása",
|
||||
"apply": "Alkalmazás",
|
||||
"cancel_event": "Időpont lemondása",
|
||||
"continue": "Folytatás",
|
||||
"confirm": "Rendben",
|
||||
"submit": "Beküldés",
|
||||
"delete": "Törlés",
|
||||
"update": "Frissítés",
|
||||
@@ -146,6 +245,7 @@
|
||||
"title": "Cím",
|
||||
"description": "Leírás",
|
||||
"duration": "Hossz",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"minutes": "Perc",
|
||||
"url": "URL",
|
||||
"hidden": "Rejtett",
|
||||
@@ -160,8 +260,9 @@
|
||||
"yearly_one": "év",
|
||||
"yearly_other": "év",
|
||||
"name": "Név",
|
||||
"logo": "Logo",
|
||||
"logo": "Logó",
|
||||
"error": "Hiba",
|
||||
"meeting_with_user": "{Event type title} {Organiser} és {Scheduler} közt",
|
||||
"type": "Típus",
|
||||
"edit": "Szerkesztés",
|
||||
"starting": "Kezdés",
|
||||
@@ -187,13 +288,26 @@
|
||||
"add": "Hozzáadás",
|
||||
"error_404": "404-es hiba",
|
||||
"default": "Alapértelmezett",
|
||||
"12_hour": "12 óra",
|
||||
"24_hour": "24 óra",
|
||||
"12_hour_short": "12ó",
|
||||
"24_hour_short": "0-24",
|
||||
"api_keys": "API kulcsok",
|
||||
"create_api_key": "API kulcs létrehozása",
|
||||
"edit_api_key": "API kulcs szerkesztése",
|
||||
"create": "Létrehozás",
|
||||
"copy": "Másolás",
|
||||
"request_reschedule_booking": "Kérés a foglalás átütemezésére",
|
||||
"reason_for_reschedule": "Ok az időpontmódosításra",
|
||||
"book_a_new_time": "Foglalj új időpontot",
|
||||
"reschedule_request_sent": "Időpontmódosítás kérelem elküldve",
|
||||
"reschedule_modal_description": "Ez lemondja a lefoglalt időpontot, értesíti a foglalót és megkéri, hogy válasszon új időpontot.",
|
||||
"reason_for_reschedule_request": "Időpontmódosítási kérelem oka",
|
||||
"send_reschedule_request": "Időpontmódosítást kérek ",
|
||||
"reschedule_booking": "Időpontmódosítás",
|
||||
"former_time": "Korábbi időpont",
|
||||
"search": "Keresés",
|
||||
"location_changed_event_type_subject": "Helyszín módosult: {{eventType}}, {{name}}, {{date}}",
|
||||
"copy_code": "Kód másolása",
|
||||
"code_copied": "Kód kimásolva!",
|
||||
"generate_api_key": "API kulcs generálása",
|
||||
@@ -202,12 +316,61 @@
|
||||
"search_giphy": "Keresls Giphy-n",
|
||||
"feedback": "Visszajelzés",
|
||||
"or_lowercase": "vagy",
|
||||
"nevermind": "Mégsem",
|
||||
"reschedule_optional": "Időpontmódosítás oka (opcionális)",
|
||||
"reschedule_placeholder": "Add mások tudtára miért van szükséged időpontmódosításra",
|
||||
"event_cancelled": "Ez az esemény le lett mondva",
|
||||
"emailed_information_about_cancelled_event": "Mindenkinek küldtünk emailt, hogy tudjanak róla.",
|
||||
"event_cancelled_trigger": "amikor az esemény le lett mondva",
|
||||
"reschedule_event_trigger": "amikor az időpont módosítva lett",
|
||||
"hour_timeUnit": "Óra",
|
||||
"minute_timeUnit": "Perc",
|
||||
"remove_app": "Alkalmazás eltávolítása",
|
||||
"yes_remove_app": "Igen, távolítsd el az alkalmazást",
|
||||
"web_conference": "Online konferencia",
|
||||
"rescheduled": "Átütemezve",
|
||||
"reminder_email": "Emlékeztető: {{eventType}}, {{name}}, {{date}}",
|
||||
"minute_one": "{{count}} perc",
|
||||
"minute_other": "{{count}} perc",
|
||||
"hour_one": "{{count}} óra",
|
||||
"hour_other": "{{count}} óra",
|
||||
"new_seat_subject": "Új résztvevő {{name}} itt: {{eventType}}, {{date}}",
|
||||
"cancelled_bookings_cannot_be_rescheduled": "Lemondott foglalásokat nem lehet átütemezni",
|
||||
"email_subject": "Email tárgy",
|
||||
"event_type_not_found": "Eseménytípus nem található",
|
||||
"location_variable": "Helyszín",
|
||||
"additional_notes_variable": "Egyéb jegyzetek",
|
||||
"timezone_variable": "Időzóna"
|
||||
"invalid_credential": "Úgy tűnik a jogosultságok lejártak vagy vissza lettek vonva a következőhöz: {{appName}}.",
|
||||
"invalid_credential_action": "Alkalmazás újratelepítése",
|
||||
"reschedule_reason": "Időpontmódosítás oka",
|
||||
"reason": "Ok",
|
||||
"email_subject_slug_replacement": "Egy csapat adminisztrátor kicserélte az eseményed /{{slug}}",
|
||||
"app_disabled_subject": "{{appName}} le lett tiltva",
|
||||
"event_no_longer_attending_subject": "Már nem vesz részt itt: {{title}}, {{date}}",
|
||||
"attendee_no_longer_attending_subject": "Egy résztvevő nem vesz részt itt: {{title}}, {{date}}",
|
||||
"attendee_no_longer_attending_subtitle": "{{name}} le lett mondva. Ez azt jelenti, hogy egy hely felszabadult erre az időpontra",
|
||||
"booking_with_payment_cancelled": "Fizetés ezért az eseményért már nem lehetséges",
|
||||
"booking_with_payment_cancelled_already_paid": "A visszatérítés ehhez a foglaláshoz úton van.",
|
||||
"booking_with_payment_cancelled_refunded": "Ez a kifizetett foglalás már vissza lett térítve.",
|
||||
"timezone_variable": "Időzóna",
|
||||
"cancel_url_variable": "Lemondás URL",
|
||||
"cancel_url_info": "URL a foglalás lemondásához",
|
||||
"reschedule_url_variable": "Időpontmódosítási URL",
|
||||
"reschedule_url_info": "Az URL az időpontmódosításhoz",
|
||||
"events_cancelled": "Lemondott Események",
|
||||
"events_rescheduled": "Átütemezett események",
|
||||
"no_show_fee_charged_email_subject": "Meg nem jelenési díj {{amount, currency}} levonva a következőért: {{title}}, {{date}}",
|
||||
"complete_your_booking_subject": "Fejezd be a foglalásod: {{title}}, {{date}}",
|
||||
"admin_org_notification_email_subject": "Új szervezet létrehozva: függőben",
|
||||
"overlay_my_calendar": "Összenézem a naptárammal",
|
||||
"overlay_my_calendar_toc": "A naptár csatlakoztatással Ön elfogadja az adatkezelési- és a felhasználási szabályzatunkat. Bármikor visszavonhatja a hozzáférést.",
|
||||
"view_overlay_calendar_events": "Nézd meg a naptáreseményeidet, hogy megelőzd az ütköző foglalásokat.",
|
||||
"booking_redirect_email_subject": "Foglalás átirányítás értesítő",
|
||||
"primary": "Elsődleges",
|
||||
"make_primary": "Legyen elsődleges",
|
||||
"add_email": "Email hozzáadása",
|
||||
"add_email_description": "Adj hozzá egy email címet, hogy lecseréld az elsődleges címed, vagy hogy használhass egy alternatív emailt az esemény típusoknél.",
|
||||
"confirm_email": "Igazold az email címed",
|
||||
"confirm_email_description": "Küldtünk egy emailt a <strong>{{email}}</strong> címre. Kattints a linkre a levélben, hogy igazold az email címed.",
|
||||
"send_event_details_to": "Esemény részleteinek küldése a következőnek"
|
||||
}
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}}: istruzioni per reimpostare la password",
|
||||
"verify_email_subject": "{{appName}}: verifica il tuo account",
|
||||
"check_your_email": "Controlla la tua e-mail",
|
||||
"old_email_address": "Vecchio indirizzo email",
|
||||
"new_email_address": "Nuovo indirizzo email",
|
||||
"verify_email_page_body": "Abbiamo inviato un'e-mail a {{email}}. È importante verificare il tuo indirizzo e-mail per garantire la consegna delle e-mail e delle notifiche del calendario {{appName}}.",
|
||||
"verify_email_banner_body": "Verifica il tuo indirizzo e-mail per garantire la consegna delle e-mail e delle notifiche del calendario",
|
||||
"verify_email_email_header": "Verifica il tuo indirizzo e-mail",
|
||||
"verify_email_email_button": "Verifica l'e-mail",
|
||||
"verify_email_change_description": "Hai recentemente chiesto di cambiare l'indirizzo email che utilizzi per accedere al tuo account {{appName}}. Fai clic sul pulsante qui sotto pe confermare il tuo nuovo indirizzo email.",
|
||||
"verify_email_change_success_toast": "Indirizzo email aggiornato: {{email}}",
|
||||
"verify_email_change_failure_toast": "Aggiornamento email non riuscito.",
|
||||
"change_of_email": "Verifica il tuo nuovo indirizzo email per {{appName}}",
|
||||
"change_of_email_toast": "Abbiamo inviato un link di verifica a {{email}}. Aggiorneremo il tuo indirizzo email quando farai clic sul link.",
|
||||
"copy_somewhere_safe": "Salva questa chiave API in un posto sicuro. Non potrai visualizzarla in seguito.",
|
||||
"verify_email_email_body": "Verifica il tuo indirizzo e-mail facendo clic sul pulsante qui sotto.",
|
||||
"verify_email_by_code_email_body": "Verifica il tuo indirizzo e-mail utilizzando il codice sottostante.",
|
||||
@@ -56,6 +63,17 @@
|
||||
"a_refund_failed": "Un rimborso non è riuscito",
|
||||
"awaiting_payment_subject": "In attesa del pagamento: {{title}} il {{date}}",
|
||||
"meeting_awaiting_payment": "La tua riunione è in attesa di pagamento",
|
||||
"dark_theme_contrast_error": "Il colore del tema scuro non supera il controllo del contrasto. Ti consigliamo di cambiare questo colore in modo che i pulsanti siano più visibili.",
|
||||
"light_theme_contrast_error": "Il colore del tema chiaro non supera il controllo del contrasto. Ti consigliamo di cambiare questo colore in modo che i pulsanti siano più visibili.",
|
||||
"payment_not_created_error": "Non è stato possibile creare il pagamento",
|
||||
"couldnt_charge_card_error": "Non è stato possibile addebitare la carta per il pagamento",
|
||||
"no_available_users_found_error": "Nessun utente disponibile. Puoi provare un altro intervallo temporale?",
|
||||
"request_body_end_time_internal_error": "Errore interno. La richiesta non contiene l'ora finale",
|
||||
"create_calendar_event_error": "Impossibile creare l'evento nel calendario dell'organizzatore",
|
||||
"update_calendar_event_error": "Impossibile aggiornare l'evento nel calendario.",
|
||||
"delete_calendar_event_error": "Impossibile eliminare l'evento dal calendario.",
|
||||
"already_signed_up_for_this_booking_error": "Ti sei già registrato per questa prenotazione.",
|
||||
"hosts_unavailable_for_booking": "Alcuni degli organizzatori non sono disponibili per la prenotazione.",
|
||||
"help": "Guida",
|
||||
"price": "Prezzo",
|
||||
"paid": "Pagato",
|
||||
@@ -101,6 +119,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Azione richiesta per la riprogrammazione: prenota un nuovo orario per {{eventType}} con {{name}}",
|
||||
"hi_user_name": "Ciao {{name}}",
|
||||
"ics_event_title": "{{eventType}} con {{name}}",
|
||||
"please_book_a_time_sometime_later": "Oops, non siamo riusciti a collegarti. Pianifica una chiamata futura",
|
||||
"new_event_subject": "Richiesta di nuovo evento: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Entra da {{entryPoint}}",
|
||||
"notes": "Osservazioni",
|
||||
@@ -117,15 +136,20 @@
|
||||
"meeting_id": "Meeting ID",
|
||||
"meeting_password": "Password del meeting",
|
||||
"meeting_url": "URL del Meeting",
|
||||
"meeting_url_not_found": "URL della riunione non trovato",
|
||||
"token_not_found": "Token non trovato",
|
||||
"some_other_host_already_accepted_the_meeting": "Qualche altro host ha già accettato la riunione. Vuoi comunque partecipare? <1>Prosegui con la riunione</1>",
|
||||
"meeting_request_rejected": "La tua richiesta di riunione è stata rifiutata",
|
||||
"rejected_event_type_with_organizer": "Rifiutato: {{eventType}} con {{organizer}} il {{date}}",
|
||||
"hi": "Ciao",
|
||||
"join_team": "Unisciti al team",
|
||||
"manage_this_team": "Gestisci questo team",
|
||||
"team_info": "Informazioni sul team",
|
||||
"join_meeting": "Partecipa alla riunione",
|
||||
"request_another_invitation_email": "Se preferisci non usare {{toEmail}} come email {{appName}} o già hai un account {{appName}}, per favore richiedi un altro invito a quella email.",
|
||||
"you_have_been_invited": "Sei stato invitato ad unirti al team {{teamName}}",
|
||||
"user_invited_you": "{{user}} ti ha invitato a unirti a {{entity}} {{team}} su {{appName}}",
|
||||
"user_invited_you_to_subteam": "{{user}} ti ha invitato a entrare nel team {{team}} dell'organizzazione {{parentTeamName}} su {{appName}}",
|
||||
"hidden_team_member_title": "Sei nascosto in questo team",
|
||||
"hidden_team_member_message": "Il tuo posto non è pagato. Passa a Pro oppure informa il proprietario del team che può pagare il tuo posto.",
|
||||
"hidden_team_owner_message": "Per utilizzare i team ti occorre un account Pro. Sarai nascosto finché non cambierai piano.",
|
||||
@@ -229,6 +253,7 @@
|
||||
"reset_your_password": "Imposta la nuova password seguendo le istruzioni che ti sono state inviate via e-mail.",
|
||||
"email_change": "Accedi di nuovo con il nuovo indirizzo e-mail e la nuova password.",
|
||||
"create_your_account": "Crea il tuo account",
|
||||
"create_your_calcom_account": "Crea il tuo accout Cal.com",
|
||||
"sign_up": "Registrati",
|
||||
"youve_been_logged_out": "Hai fatto il log out",
|
||||
"hope_to_see_you_soon": "Speriamo di rivederti presto!",
|
||||
@@ -266,6 +291,9 @@
|
||||
"nearly_there_instructions": "Infine: qualche informazione su di te e una foto sono utili per ottenere prenotazioni e per far capire alle persone con chi stanno prenotando.",
|
||||
"set_availability_instructions": "Definisci intervalli di tempo di quando sei disponibile su una base ricorrente. Puoi crearne di più in seguito e assegnarli a calendari diversi.",
|
||||
"set_availability": "Imposta la tua disponibilità",
|
||||
"set_availbility_description": "Programma gli orari in cui vuoi essere prenotato.",
|
||||
"share_a_link_or_embed": "Condividi o pubblica un link",
|
||||
"share_a_link_or_embed_description": "Condividi il tuo link {{appName}} o inseriscilo nel tuo sito.",
|
||||
"availability_settings": "Impostazioni di disponibilità",
|
||||
"continue_without_calendar": "Continua senza calendario",
|
||||
"continue_with": "Continua con {{appName}}",
|
||||
@@ -281,6 +309,7 @@
|
||||
"welcome_to_calcom": "Benvenuto in {{appName}}",
|
||||
"welcome_instructions": "Dimmi come chiamarti e facci sapere in che timezone sei. Potrai cambiarlo dopo.",
|
||||
"connect_caldav": "Connetti al server CalDav",
|
||||
"connect_ics_feed": "Collega a un feed ICS",
|
||||
"connect": "Connetti",
|
||||
"try_for_free": "Provalo gratuitamente",
|
||||
"create_booking_link_with_calcom": "Crea il tuo link di prenotazione con {{appName}}",
|
||||
@@ -419,6 +448,7 @@
|
||||
"browse_api_documentation": "Sfoglia la nostra documentazione API",
|
||||
"leverage_our_api": "Sfrutta la nostra API per il pieno controllo e la personalizzabilità.",
|
||||
"create_webhook": "Crea Webhook",
|
||||
"instant_meeting": "Riunione immediata creata",
|
||||
"booking_cancelled": "Prenotazione cancellata",
|
||||
"booking_rescheduled": "Prenotazione Riprogrammata",
|
||||
"recording_ready": "Il link per scaricare la registrazione è pronto",
|
||||
@@ -533,11 +563,13 @@
|
||||
"enter_number_between_range": "Insrisci un numero compreso tra 1 e {{maxOccurences}}",
|
||||
"email_address": "Indirizzo email",
|
||||
"enter_valid_email": "Immettere un indirizzo e-mail valido",
|
||||
"please_schedule_future_call": " Pianifica una chiamata futura se non siamo disponibili entro {{seconds}} secondi",
|
||||
"location": "Luogo",
|
||||
"address": "Indirizzo",
|
||||
"enter_address": "Immettere l'indirizzo",
|
||||
"in_person_attendee_address": "Di persona (indirizzo del partecipante)",
|
||||
"yes": "Sì",
|
||||
"no": "No",
|
||||
"additional_notes": "Note aggiuntive",
|
||||
"booking_fail": "Impossibile prenotare la riunione.",
|
||||
"reschedule_fail": "Impossibile riprogrammare la riunione.",
|
||||
@@ -591,6 +623,7 @@
|
||||
"new_member": "Nuovo Membro",
|
||||
"invite": "Invita",
|
||||
"add_team_members": "Aggiungi membri del team",
|
||||
"add_org_members": "Aggiungi membri",
|
||||
"add_team_members_description": "Invita altri utenti a unirsi al tuo team",
|
||||
"add_team_member": "Aggiungi un membro del team",
|
||||
"invite_new_member": "Invita un nuovo membro",
|
||||
@@ -605,6 +638,7 @@
|
||||
"hide_book_a_team_member_description": "Nasconde il pulsante Prenota un membro del team nelle pagine pubbliche.",
|
||||
"danger_zone": "Zona pericolosa",
|
||||
"account_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione dell'account.",
|
||||
"team_deletion_cannot_be_undone": "Attenzione. Non è possibile annullare l'eliminazione del team",
|
||||
"back": "Indietro",
|
||||
"cancel": "Annulla",
|
||||
"cancel_all_remaining": "Annulla tutti i rimanenti",
|
||||
@@ -655,6 +689,7 @@
|
||||
"default_duration": "Durata predefinita",
|
||||
"default_duration_no_options": "Scegliere prima delle durate disponibili",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"minutes": "Minuti",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Ciclo di riunioni tra più membri del team.",
|
||||
@@ -667,6 +702,7 @@
|
||||
"add_members": "Aggiungi membri...",
|
||||
"no_assigned_members": "Nessun membro assegnato",
|
||||
"assigned_to": "Assegnato a",
|
||||
"you_must_be_logged_in_to": "Devi effettuare l'accesso a {{url}}",
|
||||
"start_assigning_members_above": "Inizia ad assegnare membri in alto",
|
||||
"locked_fields_admin_description": "I membri non potranno fare modifiche",
|
||||
"locked_fields_member_description": "Questa opzione è stata bloccata dall'amministratore del team",
|
||||
@@ -771,6 +807,7 @@
|
||||
"additional_input_description": "Chiedi al creatore del programma di inserire dei campi di input aggiuntivi prima che la prenotazione sia confermata",
|
||||
"label": "Etichetta",
|
||||
"placeholder": "Segnaposto",
|
||||
"display_add_to_calendar_organizer": "Visualizza l'email \"Aggiungi al calendario\" come organizzatore",
|
||||
"type": "Tipo",
|
||||
"edit": "Modifica",
|
||||
"add_input": "Aggiungi un input",
|
||||
@@ -779,6 +816,9 @@
|
||||
"requires_confirmation_description": "La prenotazione deve essere confermata manualmente prima di essere inviata alle integrazioni e che venga inviata una e-mail di conferma.",
|
||||
"recurring_event": "Evento ricorrente",
|
||||
"recurring_event_description": "Le persone possono iscriversi a eventi ricorrenti",
|
||||
"cannot_be_used_with_paid_event_types": "Non può essere utilizzato con i tipi di evento a pagamento",
|
||||
"warning_payment_instant_meeting_event": "Le riunioni immediate non sono ancora disponibili con gli eventi ricorrenti e le app di pagamento",
|
||||
"warning_instant_meeting_experimental": "Sperimentale: gli eventi Riunione immediata al momento sono in fase di sperimentazione.",
|
||||
"starting": "Inizio",
|
||||
"disable_guests": "Disabilita Ospiti",
|
||||
"disable_guests_description": "Disabilita l'aggiunta di ulteriori ospiti durante la prenotazione.",
|
||||
@@ -787,6 +827,7 @@
|
||||
"private_link_label": "Link privato",
|
||||
"private_link_hint": "Il tuo link privato verrà rigenerato dopo ogni utilizzo",
|
||||
"copy_private_link": "Copia link privato",
|
||||
"copy_private_link_to_event": "Copia link privato nell'evento",
|
||||
"private_link_description": "Genera un URL privato da condividere senza mostrare il tuo nome utente {{appName}}",
|
||||
"invitees_can_schedule": "Gli inviti possono programmare",
|
||||
"date_range": "Range di date",
|
||||
@@ -846,6 +887,7 @@
|
||||
"next_step": "Salta passo",
|
||||
"prev_step": "Passo precedente",
|
||||
"install": "Installa",
|
||||
"start_paid_trial": "Inizia la prova gratuita",
|
||||
"installed": "Installato",
|
||||
"active_install_one": "{{count}} installazione attiva",
|
||||
"active_install_other": "{{count}} installazioni attive",
|
||||
@@ -860,6 +902,7 @@
|
||||
"toggle_calendars_conflict": "Attiva/disattiva i calendari in cui desideri controllare i conflitti per evitare doppie prenotazioni.",
|
||||
"connect_additional_calendar": "Collega calendario aggiuntivo",
|
||||
"calendar_updated_successfully": "Calendario aggiornato correttamente",
|
||||
"check_here": "Controlla qui",
|
||||
"conferencing": "Conferenze",
|
||||
"calendar": "Calendario",
|
||||
"payments": "Pagamenti",
|
||||
@@ -1031,6 +1074,9 @@
|
||||
"user_impersonation_heading": "Accesso con altra identità",
|
||||
"user_impersonation_description": "Consente al nostro team di supporto di effettuare l'accesso temporaneo a tuo nome per risolvere rapidamente gli eventuali problemi da te segnalati.",
|
||||
"team_impersonation_description": "Consente ai proprietari/amministratori del tuo team di accedere temporaneamente con il tuo nome.",
|
||||
"cal_signup_description": "Gratuito per le singole pesone. Piani collettivi per le funzionalità di collaborazione.",
|
||||
"make_org_private": "Rendi l'organizzazione privata",
|
||||
"make_org_private_description": "Quando questa opzione è attivata, i membri della tua organizzazione non possono vedere gli altri membri.",
|
||||
"make_team_private": "Rendi il team privato",
|
||||
"make_team_private_description": "Quando questa opzione è attivata, i membri del tuo team non potranno vedere gli altri membri del team.",
|
||||
"you_cannot_see_team_members": "Non è possibile vedere tutti i membri di un team privato.",
|
||||
@@ -1090,6 +1136,7 @@
|
||||
"developer_documentation": "Documentazione sviluppatore",
|
||||
"get_in_touch": "Contattaci",
|
||||
"contact_support": "Contatta il supporto",
|
||||
"premium_support": "Assistenza Premium",
|
||||
"community_support": "Supporto della community",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Grazie per il tuo feedback!",
|
||||
@@ -1253,6 +1300,7 @@
|
||||
"default_calendar_selected": "Calendario predefinito",
|
||||
"hide_from_profile": "Nascondi nel profilo",
|
||||
"event_setup_tab_title": "Impostazione evento",
|
||||
"availability_not_found_in_schedule_error": "Nessuna disponibilità trovata nel programma",
|
||||
"event_limit_tab_title": "Limiti",
|
||||
"event_limit_tab_description": "Con quale frequenza è possibile fare una prenotazione",
|
||||
"event_advanced_tab_description": "Impostazioni calendario e altro...",
|
||||
@@ -1273,6 +1321,7 @@
|
||||
"do_this": "Esegui questa operazione",
|
||||
"turn_off": "Disattiva",
|
||||
"turn_on": "Attiva",
|
||||
"cancelled_bookings_cannot_be_rescheduled": "Le prenotazioni annullate non possono essere riprogrammate",
|
||||
"settings_updated_successfully": "Impostazioni aggiornate correttamente",
|
||||
"error_updating_settings": "Errore durante l'aggiornamento delle impostazioni",
|
||||
"personal_cal_url": "Il mio URL personale di {{appName}}",
|
||||
@@ -1296,6 +1345,7 @@
|
||||
"customize_your_brand_colors": "Personalizza la tua pagina di prenotazione con i colori del tuo marchio.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Rimuove tutte le scritte con il marchio {{appName}}, ad es., 'Powered by {{appName}}.'",
|
||||
"instant_meeting_with_title": "Riunione immediata con {{name}}",
|
||||
"profile_picture": "Immagine del profilo",
|
||||
"upload": "Carica",
|
||||
"add_profile_photo": "Aggiungi foto del profilo",
|
||||
@@ -1351,6 +1401,7 @@
|
||||
"event_name_info": "Nome del tipo di evento",
|
||||
"event_date_info": "Data dell'evento",
|
||||
"event_time_info": "Ora di inizio dell'evento",
|
||||
"event_type_not_found": "EventType non trovato",
|
||||
"location_variable": "Luogo",
|
||||
"location_info": "Luogo dell'evento",
|
||||
"additional_notes_variable": "Note aggiuntive",
|
||||
@@ -1393,6 +1444,7 @@
|
||||
"slot_length": "Durata della fascia oraria",
|
||||
"booking_appearance": "Aspetto prenotazione",
|
||||
"appearance_team_description": "Gestisci le impostazioni di aspetto delle prenotazioni del tuo team",
|
||||
"appearance_org_description": "Imposta l'aspetto delle prenotazioni per la tua organizzazione",
|
||||
"only_owner_change": "Solo il proprietario del team può apportare modifiche alla prenotazione del team ",
|
||||
"team_disable_cal_branding_description": "Rimuove tutte le scritte con il marchio {{appName}}, ad es., 'Powered by {{appName}}'",
|
||||
"invited_by_team": "{{teamName}} ti ha invitato a unirti al team come {{role}}",
|
||||
@@ -1446,6 +1498,7 @@
|
||||
"password_updated": "Password aggiornata!",
|
||||
"pending_payment": "Pagamento in sospeso",
|
||||
"pending_invites": "Inviti in sospeso",
|
||||
"pending_organization_invites": "Inviti in sospeso dell'organizzazione",
|
||||
"not_on_cal": "Non presente su {{appName}}",
|
||||
"no_calendar_installed": "Nessun calendario installato",
|
||||
"no_calendar_installed_description": "Non hai ancora collegato alcun calendario",
|
||||
@@ -1457,6 +1510,8 @@
|
||||
"report_app": "Segnala app",
|
||||
"limit_booking_frequency": "Limita frequenza di prenotazione",
|
||||
"limit_booking_frequency_description": "Indica quante volte è possibile prenotare questo evento",
|
||||
"limit_booking_only_first_slot": "Limita prenotazione solo prima fascia",
|
||||
"limit_booking_only_first_slot_description": "Consenti la prenotazione solo nella prima fascia oraria di ogni giorno",
|
||||
"limit_total_booking_duration": "Limita durata totale prenotazione",
|
||||
"limit_total_booking_duration_description": "Indicare la quantità di tempo totale disponibile per prenotare questo evento",
|
||||
"add_limit": "Aggiungi limite",
|
||||
@@ -1524,10 +1579,14 @@
|
||||
"your_org_disbanded_successfully": "La tua organizzazione è stata sciolta",
|
||||
"error_creating_team": "Errore durante la creazione del team",
|
||||
"you": "Tu",
|
||||
"or_continue_with": "Oppure continua con",
|
||||
"resend_email": "Invia di nuovo l'e-mail",
|
||||
"member_already_invited": "Membro già invitato",
|
||||
"already_in_use_error": "Nome utente già in uso",
|
||||
"enter_email_or_username": "Immettere un indirizzo e-mail o un nome utente",
|
||||
"enter_email": "Inserisci un'email",
|
||||
"enter_emails": "Inserisci email",
|
||||
"too_many_invites": "Puoi invitare un massimo di {{nbUsers}} utenti alla volta.",
|
||||
"team_name_taken": "Nome già utilizzato",
|
||||
"must_enter_team_name": "Necessario immettere un nome per il team",
|
||||
"team_url_required": "Immettere un URL per il team",
|
||||
@@ -1607,6 +1666,7 @@
|
||||
"individual": "Persona",
|
||||
"all_bookings_filter_label": "Tutte le prenotazioni",
|
||||
"all_users_filter_label": "Tutti gli utenti",
|
||||
"all_event_types_filter_label": "Tutti i tipi di evento",
|
||||
"your_bookings_filter_label": "Le tue prenotazioni",
|
||||
"meeting_url_variable": "URL della riunione",
|
||||
"meeting_url_info": "URL dell'evento, dell'incontro o della conferenza",
|
||||
@@ -1703,9 +1763,11 @@
|
||||
"organizer_timezone": "Fuso orario organizzatore",
|
||||
"email_user_cta": "Visualizza invito",
|
||||
"email_no_user_invite_heading_team": "Hai ricevuto un invito a entrare a far parte del team di {{appName}}",
|
||||
"email_no_user_invite_heading_subteam": "Sei stato invitato a partecipare a un team dell'organizzazione {{parentTeamName}}",
|
||||
"email_no_user_invite_heading_org": "Hai ricevuto un invito a entrare a far parte dell'organizzazione {{appName}}",
|
||||
"email_no_user_invite_subheading": "{{invitedBy}} ti ha invitato a unirti al suo team su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette al tuo team di pianificare le riunioni senza scambiare decine di e-mail.",
|
||||
"email_user_invite_subheading_team": "{{invitedBy}} ti ha invitato a unirti al suo team `{{teamName}}` su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette a te e al tuo team di pianificare le riunioni senza scambiare decine di e-mail.",
|
||||
"email_user_invite_subheading_subteam": "{{invitedBy}} ti ha invitato a entrare nel team {{teamName}} della sua organizzazione {{parentTeamName}} su {{appName}}. {{appName}} è lo strumento di programmazione degli eventi che consente a te e al tuo team di programmare riunioni senza lo scambio di infinite email.",
|
||||
"email_user_invite_subheading_org": "{{invitedBy}} ti ha invitato a unirti alla sua organizzazione `{{teamName}}` su {{appName}}. {{appName}} è uno strumento di pianificazione di eventi che permette a te e alla tua organizzazione di pianificare le riunioni senza scambiare decine di e-mail.",
|
||||
"email_no_user_invite_steps_intro": "Ti assisteremo nei passaggi iniziali e potrai rapidamente pianificare eventi per il/la {{entity}} senza fatica.",
|
||||
"email_no_user_step_one": "Scegli il tuo nome utente",
|
||||
@@ -1838,7 +1900,9 @@
|
||||
"review_event_type": "Rivedi tipo di evento",
|
||||
"looking_for_more_analytics": "Hai bisogno di più dati analitici?",
|
||||
"looking_for_more_insights": "Ti servono più Insights?",
|
||||
"filters": "Filtri",
|
||||
"add_filter": "Aggiungi filtro",
|
||||
"remove_filters": "Cancella tutti i filtri",
|
||||
"select_user": "Seleziona utente",
|
||||
"select_event_type": "Seleziona tipo di evento",
|
||||
"select_date_range": "Seleziona intervallo di date",
|
||||
@@ -1929,6 +1993,7 @@
|
||||
"connect_google_workspace": "Collega Google Workspace",
|
||||
"google_workspace_admin_tooltip": "Per usare questa funzione è necessario essere amministratori di Workspace",
|
||||
"first_event_type_webhook_description": "Crea il primo webhook per questo tipo di evento",
|
||||
"create_instant_meeting_webhook_description": "Crea il tuo primo webhook con \"Riunione immediata creata\" come attivatore per questo tipo di evento",
|
||||
"install_app_on": "Installa l'app su",
|
||||
"create_for": "Crea per",
|
||||
"currency": "Valuta",
|
||||
@@ -1993,6 +2058,8 @@
|
||||
"add_times_to_your_email": "Seleziona alcuni orari disponibili e incorporali nella tua e-mail",
|
||||
"select_time": "Seleziona l'ora",
|
||||
"select_date": "Seleziona la data",
|
||||
"connecting_you_to_someone": "Ti stiamo collegando con qualcuno.",
|
||||
"please_do_not_close_this_tab": "Non chiudere questa scheda",
|
||||
"see_all_available_times": "Vedi tutti gli orari disponibili",
|
||||
"org_team_names_example_1": "ad es., team di marketing",
|
||||
"org_team_names_example_2": "ad es., team vendite",
|
||||
@@ -2010,8 +2077,12 @@
|
||||
"description_requires_booker_email_verification": "Per garantire la verifica dell'indirizzo e-mail della persona che prenota prima di panificare eventi",
|
||||
"requires_confirmation_mandatory": "I messaggi di testo possono solo essere inviati ai partecipanti quando il tipo di evento richiede una conferma.",
|
||||
"organizations": "Organizzazioni",
|
||||
"upload_cal_video_logo": "Carica il logo Cal Video",
|
||||
"update_cal_video_logo": "Aggiorna il logo Cal Video",
|
||||
"cal_video_logo_upload_instruction": "Per fare in modo che il tuo logo sia visibile sullo sfondo scuro del video Cal, carica un'immagine di colore chiaro in formato PNG o SVG per mantenere la trasparenza.",
|
||||
"org_admin_other_teams": "Atri team",
|
||||
"org_admin_other_teams_description": "Qui puoi vedere i team di cui non fai parte all'interno della tua organizzazione. Puoi entrare a far parte di tali team se necessario.",
|
||||
"not_part_of_org": "Non fai parte di alcuna organizzazione",
|
||||
"no_other_teams_found": "Nessun altro team trovato",
|
||||
"no_other_teams_found_description": "Non ci sono altri team in questa organizzazione.",
|
||||
"attendee_first_name_variable": "Nome del partecipante",
|
||||
@@ -2047,7 +2118,9 @@
|
||||
"org_error_processing": "Si è verificato un errore durante l'elaborazione di questa organizzazione",
|
||||
"orgs_page_description": "Un elenco di tutte le organizzazioni. Accettando un'organizzazione, tutti gli utenti con il rispettivo dominio e-mail potranno registrarsi SENZA verificare l'indirizzo e-mail.",
|
||||
"unverified": "Non verificato",
|
||||
"verified": "Verificato",
|
||||
"dns_missing": "DNS mancante",
|
||||
"dns_configured": "DNS Configurato",
|
||||
"mark_dns_configured": "Contrassegna DNS come configurato",
|
||||
"value": "Valore",
|
||||
"your_organization_updated_sucessfully": "L'organizzazione è stata aggiornata correttamente",
|
||||
@@ -2057,8 +2130,11 @@
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Aggiunti di recente",
|
||||
"connect_all_calendars": "Collega tutti i tuoi calendari",
|
||||
"connect_all_calendars_description": "{{appName}} legge la disponibilità da tutti i tuoi calendari esistenti.",
|
||||
"workflow_automation": "Automazione dei flussi di lavoro",
|
||||
"workflow_automation_description": "Personalizza la tua esperienza di programmazione con i flussi di lavoro",
|
||||
"scheduling_for_your_team": "Automazione dei flussi di lavoro",
|
||||
"scheduling_for_your_team_description": "Usa la programmazione collettiva e round robin per il tuo team",
|
||||
"no_members_found": "Nessun membro trovato",
|
||||
"event_setup_length_error": "Impostazione evento: la durata deve essere di almeno 1 minuto.",
|
||||
"availability_schedules": "Calendario disponibilità",
|
||||
@@ -2091,10 +2167,100 @@
|
||||
"overlay_my_calendar": "Sovrapponi il mio calendario",
|
||||
"overlay_my_calendar_toc": "Collegando il tuo calendario, accetti la nostra informativa sulla privacy e i termini di servizio. Puoi revocare l'accesso in qualsiasi momento.",
|
||||
"view_overlay_calendar_events": "Visualizza gli eventi del tuo calendario per prevenire prenotazioni in conflitto.",
|
||||
"join_event_location": "Partecipa a {{eventLocationType}}",
|
||||
"troubleshooting": "Risoluzione problemi",
|
||||
"calendars_were_checking_for_conflicts": "Calendari in cui stiamo cercando possibili conflitti",
|
||||
"availabilty_schedules": "Programmi di disponibilità",
|
||||
"manage_calendars": "Gestisci calendari",
|
||||
"manage_availability_schedules": "Gestisci programmi di disponibilità",
|
||||
"lock_timezone_toggle_on_booking_page": "Blocca fuso orario nella pagina di prenotazione",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Per bloccare il fuso orario nella pagina di prenotazione, utile per gli eventi di persona.",
|
||||
"event_setup_multiple_payment_apps_error": "Puoi avere solo un'app di pagamento abilitata per ciascun tipo di evento.",
|
||||
"number_in_international_format": "Inserisci il numero in formato internazionale.",
|
||||
"install_calendar": "Installa Calendario",
|
||||
"branded_subdomain": "Sottodominio del marchio",
|
||||
"branded_subdomain_description": "Crea un sottodominio per il tuo marchio, come acme.cal.com",
|
||||
"org_insights": "Approfondimenti a livello di organizzazione",
|
||||
"org_insights_description": "Comprendi come la tua organizzazione utilizza il tempo",
|
||||
"extensive_whitelabeling": "Assistenza per l'onboarding e supporto tecnico dedicati",
|
||||
"extensive_whitelabeling_description": "Personalizza la tua esperienza di programmazione con il tuo logo, colori e altro ancora",
|
||||
"unlimited_teams": "Numero illimitato di team",
|
||||
"unlimited_teams_description": "Aggiungi alla tua organizzazione tutti i sotto-team necessari",
|
||||
"unified_billing": "Fatturazione unificata",
|
||||
"unified_billing_description": "Indica una singola carta di credito per pagare tutti gli abbonamenti del tuo team",
|
||||
"advanced_managed_events": "Tipi di evento gestiti avanzati",
|
||||
"advanced_managed_events_description": "Indica una singola carta di credito per pagare tutti gli abbonamenti del tuo team",
|
||||
"enterprise_description": "Passa a Enterprise per creare la tua organizzazione",
|
||||
"create_your_org": "Crea la tua organizzazione",
|
||||
"create_your_org_description": "Passa a Enterprise per avere sottodominio, fatturazione unificata, approfondimenti, personalizzazione completa e altro ancora",
|
||||
"other_payment_app_enabled": "Puoi attivare solo un'app di pagamento per ciascun tipo di evento",
|
||||
"admin_delete_organization_description": "<ul><li>Verranno eliminati anche i team che sono membri di questa organizzazione, insieme ai rispettivi tipi di evento</li><li>Gli utenti che facevano parte dell'organizzazione non verranno eliminati e anche i loro tipi di evento rimarranno inalterati.</li><li>I nomi utente saranno modificati in modo che possano esistere al di fuori dell'organizzazione</li></ul>",
|
||||
"admin_delete_organization_title": "Eliminare {{organizationName}}?",
|
||||
"published": "Pubblicato",
|
||||
"unpublished": "Pubblicazione annullata",
|
||||
"publish": "Pubblica",
|
||||
"org_publish_error": "Non è stato possibile pubblicare l'organizzazione",
|
||||
"troubleshooter_tooltip": "Apri lo strumento di soluzione dei problemi per individuare il problema del tuo programma",
|
||||
"need_help": "Hai bisogno di aiuto?",
|
||||
"troubleshooter": "Risoluzione problemi",
|
||||
"please_install_a_calendar": "Installa un calendario",
|
||||
"instant_tab_title": "Prenotazione immediata",
|
||||
"instant_event_tab_description": "Consenti alle persone di prenotare immediatamente",
|
||||
"uprade_to_create_instant_bookings": "Passa a Enterprise e permetti agli ospiti di partecipare a una chiamata immediata a cui i partecipanti possono accedere direttamente. Ciò è riservato agli eventi per team",
|
||||
"dont_want_to_wait": "Non vuoi aspettare?",
|
||||
"meeting_started": "Riunione iniziata",
|
||||
"pay_and_book": "Pagare per prenotare",
|
||||
"booking_not_found_error": "Prenotazione non trovata",
|
||||
"booking_seats_full_error": "I posti sono esauriti",
|
||||
"missing_payment_credential_error": "Credenziali di pagamento mancanti",
|
||||
"missing_payment_app_id_error": "ID dell'app di pagamento mancante",
|
||||
"not_enough_available_seats_error": "La prenotazione non ha abbastanza posti disponibili",
|
||||
"user_redirect_title": "{{username}} è attualmente assente per un breve periodo.",
|
||||
"user_redirect_description": "Nel frattempo, {{profile.username}} sarà responsabile di tutte le nuove riunioni programmate per conto di {{username}}.",
|
||||
"out_of_office": "Assente",
|
||||
"out_of_office_description": "Configura le azioni nel tuo profilo mentre sei assente.",
|
||||
"send_request": "Invia richiesta",
|
||||
"start_date_and_end_date_required": "Data iniziale e data finale sono obbligatorie",
|
||||
"start_date_must_be_before_end_date": "La data iniziale deve precedere quella finale",
|
||||
"start_date_must_be_in_the_future": "La data iniziale deve essere nel futuro",
|
||||
"user_not_found": "Utente non trovato",
|
||||
"out_of_office_entry_already_exists": "La voce per l'assenza esiste già",
|
||||
"out_of_office_id_required": "L'ID della voce per l'assenza è obbligatorio",
|
||||
"booking_redirect_infinite_not_allowed": "Esiste già un reindirizzamento della prenotazione dall'utente a te.",
|
||||
"success_entry_created": "Nuova voce creata correttamente",
|
||||
"booking_redirect_email_subject": "Notifica di reindirizzamento prenotazione",
|
||||
"booking_redirect_email_title": "Notifica di reindirizzamento prenotazione",
|
||||
"booking_redirect_email_description": "Hai ricevuto un reindirizzamento delle prenotazioni da {{toName}}. I link del suo profilo rimanderanno ai tuoi, nel periodo: ",
|
||||
"success_accept_booking_redirect": "Hai accettato questa richiesta di reindirizzamento delle prenotazioni.",
|
||||
"success_reject_booking_redirect": "Hai rifiutato questa richiesta di reindirizzamento delle prenotazioni.",
|
||||
"copy_link_booking_redirect_request": "Copia link per condividere la richiesta",
|
||||
"booking_redirect_request_title": "Richiesta di reindirizzamento delle prenotazioni",
|
||||
"select_team_member": "Seleziona un membro del team",
|
||||
"going_away_title": "Devi assentarti? Segnala che il link al tuo profilo non è disponibile per un periodo.",
|
||||
"redirect_team_enabled": "Rendirizza il tuo profilo a un altro membro del team",
|
||||
"redirect_team_disabled": "Reindirizza il tuo profilo a un altro membro del team (è necessario il piano Team)",
|
||||
"out_of_office_unavailable_list": "Elenco di indisponibilità per assenza",
|
||||
"success_deleted_entry_out_of_office": "Voce eliminata",
|
||||
"temporarily_out_of_office": "Temporaneamente assente?",
|
||||
"add_a_redirect": "Aggiungi un reindirizzamento",
|
||||
"create_entry": "Crea una voce",
|
||||
"time_range": "Intervallo temporale",
|
||||
"automatically_add_all_team_members": "Aggiungi tutti i membri del team, compresi quelli futuri",
|
||||
"redirect_to": "Reindirizza a",
|
||||
"having_trouble_finding_time": "Non riesci a trovare un orario?",
|
||||
"show_more": "Mostra altro",
|
||||
"assignment_description": "Pianifica le riunioni quando tutti sono disponibili, oppure ruota tra i membri del tuo team",
|
||||
"lowest": "più basso",
|
||||
"low": "basso",
|
||||
"medium": "medio",
|
||||
"high": "alto",
|
||||
"Highest": "più alto",
|
||||
"send_booker_to": "Rimanda la persona che prenota a",
|
||||
"set_priority": "Imposta priorità",
|
||||
"priority_for_user": "Priorità per {{userName}}",
|
||||
"change_priority": "cambia priorità",
|
||||
"field_identifiers_as_variables": "Usa gli identificatori di campo come variabili per il reindirizzamento dell'evento personalizzato",
|
||||
"field_identifiers_as_variables_with_example": "Usa gli identificatori di campo come variabili per il reindirizzamento dell'evento personalizzato (es: {{variable}})",
|
||||
"account_already_linked": "L'account è già collegato",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}}: Instruções para redefinir sua senha",
|
||||
"verify_email_subject": "{{appName}}: Verificar sua conta",
|
||||
"check_your_email": "Verifique seu e-mail",
|
||||
"old_email_address": "E-mail antigo",
|
||||
"new_email_address": "Novo e-mail",
|
||||
"verify_email_page_body": "Enviamos um e-mail para {{email}}. É importante verificar seu endereço de e-mail para garantir o melhor envio de e-mail e calendário do {{appName}}.",
|
||||
"verify_email_banner_body": "Verifique seu endereço de e-mail para garantir o fornecimento do melhor e-mail e calendário",
|
||||
"verify_email_email_header": "Verifique seu endereço de e-mail",
|
||||
"verify_email_email_button": "Verificar e-mail",
|
||||
"verify_email_change_description": "Você solicitou recentemente a alteração do endereço de e-mail que você usa para entrar em sua conta {{appName}}. Por favor, clique no botão abaixo para confirmar o seu novo endereço de e-mail.",
|
||||
"verify_email_change_success_toast": "Seu e-mail foi atualizado para {{email}}",
|
||||
"verify_email_change_failure_toast": "Não foi possível atualizar o e-mail.",
|
||||
"change_of_email": "Verifique seu novo endereço de e-mail da sua conta {{appName}}",
|
||||
"change_of_email_toast": "Enviamos um link de verificação para {{email}}. Atualizaremos seu endereço de e-mail quando você clicar neste link.",
|
||||
"copy_somewhere_safe": "Salve esta chave de API em um lugar seguro. Você não será capaz de vê-la novamente.",
|
||||
"verify_email_email_body": "Por favor, verifique seu endereço de e-mail clicando no botão abaixo.",
|
||||
"verify_email_by_code_email_body": "Por favor, verifique seu endereço de e-mail usando o código abaixo.",
|
||||
@@ -56,6 +63,17 @@
|
||||
"a_refund_failed": "Um reembolso falhou",
|
||||
"awaiting_payment_subject": "Aguardando pagamento: {{title}} em {{date}}",
|
||||
"meeting_awaiting_payment": "Sua reunião está aguardando o pagamento",
|
||||
"dark_theme_contrast_error": "A cor do tema escuro não foi aprovada pela verificação de contraste. Recomendamos que você altere esta cor para que seus botões fiquem mais visíveis.",
|
||||
"light_theme_contrast_error": "A cor do tema claro não foi aprovada pela verificação de contraste. Recomendamos que você altere esta cor para que seus botões fiquem mais visíveis.",
|
||||
"payment_not_created_error": "O pagamento não pôde ser criado",
|
||||
"couldnt_charge_card_error": "Não foi possível cobrar o cartão para o pagamento",
|
||||
"no_available_users_found_error": "Nenhum usuário disponível foi encontrado. Você poderia tentar outra faixa horária?",
|
||||
"request_body_end_time_internal_error": "Erro interno. O corpo da solicitação não contém a hora final",
|
||||
"create_calendar_event_error": "Não foi possível criar um evento no calendário do organizador",
|
||||
"update_calendar_event_error": "Não foi possível atualizar o evento.",
|
||||
"delete_calendar_event_error": "Não foi possível deletar o evento.",
|
||||
"already_signed_up_for_this_booking_error": "Você já está inscrito para esta reserva.",
|
||||
"hosts_unavailable_for_booking": "Alguns dos anfitriões não estão disponíveis para reserva.",
|
||||
"help": "Ajuda",
|
||||
"price": "Preço",
|
||||
"paid": "Pago",
|
||||
@@ -101,6 +119,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Ação de reagendamento necessária: reserve outro horário para {{eventType}} com {{name}}",
|
||||
"hi_user_name": "Olá {{name}}",
|
||||
"ics_event_title": "{{eventType}} com {{name}}",
|
||||
"please_book_a_time_sometime_later": "Ops, não foi possível conectá-lo desta vez. Por favor, agende uma chamada mais tarde",
|
||||
"new_event_subject": "Novo evento: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Junte-se através de {{entryPoint}}",
|
||||
"notes": "Notas",
|
||||
@@ -117,15 +136,20 @@
|
||||
"meeting_id": "ID da reunião",
|
||||
"meeting_password": "Senha da reunião",
|
||||
"meeting_url": "URL da reunião",
|
||||
"meeting_url_not_found": "URL da reunião não encontrada",
|
||||
"token_not_found": "Token não encontrado",
|
||||
"some_other_host_already_accepted_the_meeting": "Algum outro anfitrião já aceitou a reunião. Ainda deseja entrar? <1>Entrar na reunião</1>",
|
||||
"meeting_request_rejected": "A sua solicitação de reunião foi rejeitada",
|
||||
"rejected_event_type_with_organizer": "Rejeitado: {{eventType}} com {{organizer}} em {{date}}",
|
||||
"hi": "Olá",
|
||||
"join_team": "Entrar no time",
|
||||
"manage_this_team": "Gerenciar esta equipe",
|
||||
"team_info": "Informações da equipe",
|
||||
"join_meeting": "Entrar na reunião",
|
||||
"request_another_invitation_email": "Se não quiser utilizar {{toEmail}} como email do {{appName}}, ou já tem uma conta em {{appName}}, por favor peça outro convite para esse email.",
|
||||
"you_have_been_invited": "Você recebeu um convite para se juntar ao time {{teamName}}",
|
||||
"user_invited_you": "{{user}} convidou para ingressar na equipe {{team}} de {{entity}} em {{appName}}",
|
||||
"user_invited_you_to_subteam": "{{user}} convidou você para se juntar à equipe {{team}} da organização {{parentTeamName}} no {{appName}}",
|
||||
"hidden_team_member_title": "Você está oculto nesta equipe",
|
||||
"hidden_team_member_message": "Seu assento não é pago, atualize para a versão Pro ou avise o proprietário da equipe de que é possível pagar pelo seu assento.",
|
||||
"hidden_team_owner_message": "É preciso ter uma conta Pro para usar equipes, você permanecerá oculto até a atualização.",
|
||||
@@ -229,6 +253,7 @@
|
||||
"reset_your_password": "Defina sua nova senha com as instruções enviadas para seu endereço de e-mail.",
|
||||
"email_change": "Volte a entrar com seu novo endereço de e-mail e senha.",
|
||||
"create_your_account": "Criar sua conta",
|
||||
"create_your_calcom_account": "Crie sua conta Cal.com",
|
||||
"sign_up": "Cadastro",
|
||||
"youve_been_logged_out": "A sessão foi encerrada",
|
||||
"hope_to_see_you_soon": "Esperamos te ver novamente em breve!",
|
||||
@@ -266,6 +291,9 @@
|
||||
"nearly_there_instructions": "Por último, uma breve descrição de você mesmo e uma foto ajudam a conseguir agendamentos e a mostrar às pessoas com quem elas estão agendando.",
|
||||
"set_availability_instructions": "Definir períodos em que você está disponível de forma recorrente. Mais tarde você poderá criar outros períodos e atribuí-los a diferentes calendários.",
|
||||
"set_availability": "Definir disponibilidade",
|
||||
"set_availbility_description": "Defina os horários que você quer que estejam disponíveis para reservas.",
|
||||
"share_a_link_or_embed": "Compartilhe um link ou incorpore",
|
||||
"share_a_link_or_embed_description": "Compartilhe o link da sua conta {{appName}} ou incorpore no seu site.",
|
||||
"availability_settings": "Configurações de disponibilidade",
|
||||
"continue_without_calendar": "Continuar sem calendário",
|
||||
"continue_with": "Continuar com {{appName}}",
|
||||
@@ -281,6 +309,7 @@
|
||||
"welcome_to_calcom": "Bem vindo ao {{appName}}",
|
||||
"welcome_instructions": "Diga como devemos te chamar e qual o seu fuso horário. Você poderá editar isto mais tarde.",
|
||||
"connect_caldav": "Conectar a um servidor CalDav",
|
||||
"connect_ics_feed": "Conectar a um feed ICS",
|
||||
"connect": "Conectar",
|
||||
"try_for_free": "Experimente gratuitamente",
|
||||
"create_booking_link_with_calcom": "Crie o seu próprio link para reservas com {{appName}}",
|
||||
@@ -419,6 +448,7 @@
|
||||
"browse_api_documentation": "Consulte a nossa documentação da API",
|
||||
"leverage_our_api": "Utilize nossa API para controle e personalização completa.",
|
||||
"create_webhook": "Criar Webhook",
|
||||
"instant_meeting": "Reunião instantânea criada",
|
||||
"booking_cancelled": "Reserva cancelada",
|
||||
"booking_rescheduled": "Reserva Reagendada",
|
||||
"recording_ready": "Link para download de gravação pronto",
|
||||
@@ -533,6 +563,7 @@
|
||||
"enter_number_between_range": "Por favor, insira um número entre 1 e {{maxOccurences}}",
|
||||
"email_address": "Endereço de e-mail",
|
||||
"enter_valid_email": "Insira um e-mail válido",
|
||||
"please_schedule_future_call": " Por favor, agende uma chamada futura se não estivermos disponíveis em {{seconds}} segundos",
|
||||
"location": "Local",
|
||||
"address": "Local",
|
||||
"enter_address": "Insira o endereço",
|
||||
@@ -592,6 +623,7 @@
|
||||
"new_member": "Novo Membro",
|
||||
"invite": "Convidar",
|
||||
"add_team_members": "Adicionar membros da equipe",
|
||||
"add_org_members": "Adicionar membros",
|
||||
"add_team_members_description": "Convidar outros para se juntar à sua equipe",
|
||||
"add_team_member": "Adicionar membro da equipe",
|
||||
"invite_new_member": "Convidar um novo membro",
|
||||
@@ -606,6 +638,7 @@
|
||||
"hide_book_a_team_member_description": "Oculte o botão de agendar um membro da equipe em suas páginas públicas.",
|
||||
"danger_zone": "Zona de perigo",
|
||||
"account_deletion_cannot_be_undone": "Cuidado. A exclusão da conta não pode ser desfeita.",
|
||||
"team_deletion_cannot_be_undone": "Cuidado. A exclusão de uma equipe não pode ser desfeita",
|
||||
"back": "Voltar",
|
||||
"cancel": "Cancelar",
|
||||
"cancel_all_remaining": "Cancelar todos os restantes",
|
||||
@@ -668,6 +701,7 @@
|
||||
"add_members": "Adicionar membros...",
|
||||
"no_assigned_members": "Nenhum membro atribuído",
|
||||
"assigned_to": "Atribuído a",
|
||||
"you_must_be_logged_in_to": "Você deve estar logado em {{url}}",
|
||||
"start_assigning_members_above": "Começar a atribuir os membros acima",
|
||||
"locked_fields_admin_description": "Os membros não poderão editar isto",
|
||||
"locked_fields_member_description": "Esta opção foi bloqueada pelo administrador da equipe",
|
||||
@@ -772,6 +806,7 @@
|
||||
"additional_input_description": "Solicitar que o agendador insira entradas adicionais antes que o pedido seja confirmado",
|
||||
"label": "Etiqueta",
|
||||
"placeholder": "Preenchimento",
|
||||
"display_add_to_calendar_organizer": "Exibir o e-mail 'Adicionar ao calendário' como organizador",
|
||||
"type": "Tipo",
|
||||
"edit": "Editar",
|
||||
"add_input": "Adicionar um campo",
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}}: Instruções de redefinição da senha",
|
||||
"verify_email_subject": "{{appName}}: Verifique a sua conta",
|
||||
"check_your_email": "Verifique o seu e-mail",
|
||||
"old_email_address": "E-mail antigo",
|
||||
"new_email_address": "Novo e-mail",
|
||||
"verify_email_page_body": "Enviámos um e-mail para {{email}}. É importante verificar o seu endereço de e-mail para garantir que receberá as comunicações de {{appName}}.",
|
||||
"verify_email_banner_body": "Confirme o seu endereço de e-mail para garantir a melhor entrega possível de e-mail e de agenda",
|
||||
"verify_email_email_header": "Confirme o seu endereço de e-mail",
|
||||
"verify_email_email_button": "Confirmar o seu e-mail",
|
||||
"verify_email_change_description": "Solicitou recentemente a alteração do endereço de e-mail que utiliza para entrar na sua conta {{appName}}. Clique no botão abaixo para confirmar o seu novo endereço de e-mail.",
|
||||
"verify_email_change_success_toast": "Atualizou o seu e-mail para {{email}}",
|
||||
"verify_email_change_failure_toast": "Falha ao atualizar o e-mail.",
|
||||
"change_of_email": "Confirme o seu novo endereço de e-mail para {{appName}}",
|
||||
"change_of_email_toast": "Enviámos uma ligação de confirmação para {{email}}. Atualizaremos o seu endereço de e-mail após clicar nessa ligação.",
|
||||
"copy_somewhere_safe": "Guarde esta chave da API num local seguro. Não poderá consultar a mesma novamente.",
|
||||
"verify_email_email_body": "Confirme o seu endereço de e-mail clicando no botão abaixo.",
|
||||
"verify_email_by_code_email_body": "Confirme o seu endereço de e-mail utilizando o código abaixo.",
|
||||
@@ -142,6 +149,7 @@
|
||||
"request_another_invitation_email": "Se não quiser utilizar {{toEmail}} como email do {{appName}}, ou já tem uma conta em {{appName}}, por favor peça outro convite para esse email.",
|
||||
"you_have_been_invited": "Recebeu um convite para juntar-se à equipa {{teamName}}",
|
||||
"user_invited_you": "{{user}} enviou um convite para se juntar a {{team}} de {{entity}} em {{appName}}",
|
||||
"user_invited_you_to_subteam": "{{user}} enviou um convite para se juntar à equipa {{team}} da organização {{parentTeamName}} em {{appName}}",
|
||||
"hidden_team_member_title": "Está oculto nesta equipa",
|
||||
"hidden_team_member_message": "O seu lugar não está pago, faça upgrade para PRO ou informe ao proprietário da equipa que pode pagar pelo seu lugar.",
|
||||
"hidden_team_owner_message": "Precisa de uma conta Pro para usar equipas, ficará oculto até que actualize.",
|
||||
@@ -245,6 +253,7 @@
|
||||
"reset_your_password": "Defina a sua nova palavra-passe seguindo as instruções enviadas para o seu endereço de e-mail.",
|
||||
"email_change": "Inicie sessão novamente com o seu novo endereço de e-mail e palavra-passe.",
|
||||
"create_your_account": "Crie a sua conta",
|
||||
"create_your_calcom_account": "Crie a sua conta Cal.com",
|
||||
"sign_up": "Registar",
|
||||
"youve_been_logged_out": "A sessão foi encerrada",
|
||||
"hope_to_see_you_soon": "Esperamos ver-nos em breve de novo!",
|
||||
@@ -282,6 +291,9 @@
|
||||
"nearly_there_instructions": "Por último, uma breve descrição sobre si e uma fotografia ajudam a obter reservas e a mostrar às pessoas com quem estão a reservar.",
|
||||
"set_availability_instructions": "Definir períodos em que está disponível numa base recorrente. Mais tarde poderá criar mais destes, e atribuí-los a diferentes calendários.",
|
||||
"set_availability": "Definir disponibilidade",
|
||||
"set_availbility_description": "Defina agendamentos para os horários que pretende que sejam reservados.",
|
||||
"share_a_link_or_embed": "Partilhe uma ligação ou incorpore",
|
||||
"share_a_link_or_embed_description": "Partilhe a sua ligação {{appName}} ou incorpore no seu site.",
|
||||
"availability_settings": "Configurações de disponibilidade",
|
||||
"continue_without_calendar": "Continuar sem calendário",
|
||||
"continue_with": "Continuar com {{appName}}",
|
||||
@@ -297,6 +309,7 @@
|
||||
"welcome_to_calcom": "Boas-vindas ao {{appName}}",
|
||||
"welcome_instructions": "Diga-nos como lhe devemos chamar e qual o seu fuso horário. Poderá editar isto mais tarde.",
|
||||
"connect_caldav": "Ligar a servidor CalDav",
|
||||
"connect_ics_feed": "Ligue-se a uma Feed ICS",
|
||||
"connect": "Ligar",
|
||||
"try_for_free": "Experimente gratuitamente",
|
||||
"create_booking_link_with_calcom": "Crie a sua própria ligação de reservas com {{appName}}",
|
||||
@@ -435,6 +448,7 @@
|
||||
"browse_api_documentation": "Consulte a nossa documentação da API",
|
||||
"leverage_our_api": "Tire partido da nossa API para controlo e personalização total.",
|
||||
"create_webhook": "Criar Webhook",
|
||||
"instant_meeting": "Reunião instantânea criada",
|
||||
"booking_cancelled": "Reserva cancelada",
|
||||
"booking_rescheduled": "Reserva Reagendada",
|
||||
"recording_ready": "Ligação de transferência da gravação pronta",
|
||||
@@ -549,6 +563,7 @@
|
||||
"enter_number_between_range": "Introduza um número entre 1 e {{maxOccurences}}",
|
||||
"email_address": "Endereço de email",
|
||||
"enter_valid_email": "Insira um e-mail válido",
|
||||
"please_schedule_future_call": " Se não estivermos disponíveis em {{seconds}} segundos, por favor, agende uma chamada para mais tarde",
|
||||
"location": "Localização",
|
||||
"address": "Local",
|
||||
"enter_address": "Especifique o endereço",
|
||||
@@ -608,6 +623,7 @@
|
||||
"new_member": "Novo Membro",
|
||||
"invite": "Convidar",
|
||||
"add_team_members": "Adicionar membros da equipa",
|
||||
"add_org_members": "Adicionar membros",
|
||||
"add_team_members_description": "Convidar outros a juntarem-se à sua equipa",
|
||||
"add_team_member": "Adicionar membro da equipa",
|
||||
"invite_new_member": "Convidar um Novo Membro",
|
||||
@@ -622,6 +638,7 @@
|
||||
"hide_book_a_team_member_description": "Ocultar o botão Reservar um Membro da equipa das suas páginas públicas.",
|
||||
"danger_zone": "Zona de perigo",
|
||||
"account_deletion_cannot_be_undone": "Cuidado. A eliminação da conta não pode ser anulada.",
|
||||
"team_deletion_cannot_be_undone": "Cuidado. A eliminação de uma equipa não pode ser anulada",
|
||||
"back": "Anterior",
|
||||
"cancel": "Cancelar",
|
||||
"cancel_all_remaining": "Cancelar todos os restantes",
|
||||
@@ -672,6 +689,7 @@
|
||||
"default_duration": "Duração predefinida",
|
||||
"default_duration_no_options": "Por favor, selecione primeiro as durações disponíveis",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"multiple_duration_timeUnit": "{{count}} $t({{unit}}_timeUnit)",
|
||||
"minutes": "Minutos",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Reuniões de ciclo entre vários membros da equipa.",
|
||||
@@ -684,6 +702,7 @@
|
||||
"add_members": "Adicionar membros...",
|
||||
"no_assigned_members": "Sem membros atribuídos",
|
||||
"assigned_to": "Atribuído a",
|
||||
"you_must_be_logged_in_to": "Deve ter sessão iniciada para {{url}}",
|
||||
"start_assigning_members_above": "Comece a atribuir membros acima",
|
||||
"locked_fields_admin_description": "Os membros não poderão editar isto",
|
||||
"locked_fields_member_description": "Esta opção foi bloqueada pelo administrador da equipa",
|
||||
@@ -788,6 +807,7 @@
|
||||
"additional_input_description": "Requerer que o responsável pelo agendamento especifique entradas complementares antes do pedido ser confirmado",
|
||||
"label": "Etiqueta",
|
||||
"placeholder": "Preenchimento",
|
||||
"display_add_to_calendar_organizer": "Mostrar o e-mail 'Adicionar ao calendário' como organizador",
|
||||
"type": "Tipo",
|
||||
"edit": "Editar",
|
||||
"add_input": "Adicionar um Campo",
|
||||
@@ -796,6 +816,9 @@
|
||||
"requires_confirmation_description": "A reserva precisa ser confirmada manualmente antes de ser enviada para as integrações e ser enviada uma mensagem de confirmação.",
|
||||
"recurring_event": "Evento recorrente",
|
||||
"recurring_event_description": "As pessoas podem subscrever para eventos recorrentes",
|
||||
"cannot_be_used_with_paid_event_types": "Não pode ser utilizado com tipos de eventos pagos",
|
||||
"warning_payment_instant_meeting_event": "As Reuniões Instantâneas ainda não são suportadas com eventos recorrentes e aplicações de pagamento",
|
||||
"warning_instant_meeting_experimental": "Experimental: atualmente os Eventos de Reuniões Instantâneas são experimentais.",
|
||||
"starting": "A iniciar",
|
||||
"disable_guests": "Desativar Convidados",
|
||||
"disable_guests_description": "Desativar a adição de convidados adicionais durante a reserva.",
|
||||
@@ -804,6 +827,7 @@
|
||||
"private_link_label": "Ligação privada",
|
||||
"private_link_hint": "A sua ligação será renovada após cada utilização",
|
||||
"copy_private_link": "Copiar ligação privada",
|
||||
"copy_private_link_to_event": "Copiar ligação privada para o evento",
|
||||
"private_link_description": "Crie um URL privado para partilhar sem expor o seu nome de utilizador do {{appName}}",
|
||||
"invitees_can_schedule": "Os convidados podem agendar",
|
||||
"date_range": "Intervalo de Datas",
|
||||
@@ -863,6 +887,7 @@
|
||||
"next_step": "Ignorar passo",
|
||||
"prev_step": "Passo anterior",
|
||||
"install": "Instalar",
|
||||
"start_paid_trial": "Iniciar avaliação gratuita",
|
||||
"installed": "Instalado",
|
||||
"active_install_one": "{{count}} instalação activa",
|
||||
"active_install_other": "{{count}} instalações activas",
|
||||
@@ -877,6 +902,7 @@
|
||||
"toggle_calendars_conflict": "Escolha os calendários que deseja verificar para evitar conflitos e duplicação de reservas.",
|
||||
"connect_additional_calendar": "Ligar um calendário adicional",
|
||||
"calendar_updated_successfully": "Calendário atualizado com sucesso",
|
||||
"check_here": "Verificar aqui",
|
||||
"conferencing": "Conferência",
|
||||
"calendar": "Calendário",
|
||||
"payments": "Pagamentos",
|
||||
@@ -1048,6 +1074,9 @@
|
||||
"user_impersonation_heading": "Representação de utilizador",
|
||||
"user_impersonation_description": "Permite à nossa equipa de suporte iniciar sessão temporariamente em seu nome para rapidamente resolver quaisquer erros que nos reporte.",
|
||||
"team_impersonation_description": "Permite que os administradores da sua equipa iniciem a sessão temporariamente em seu nome.",
|
||||
"cal_signup_description": "Gratuito para pessoas singulares. Planos de equipa para funcionalidades colaborativas.",
|
||||
"make_org_private": "Tornar a organização privada",
|
||||
"make_org_private_description": "Os membros da sua organização não poderão ver outros membros da organização quando esta opção estiver ativada.",
|
||||
"make_team_private": "Tornar a equipa privada",
|
||||
"make_team_private_description": "Os membros da sua equipa não poderão ver outros membros da equipa quando esta opção estiver ativada.",
|
||||
"you_cannot_see_team_members": "Não pode ver todos os membros da equipa de uma equipa privada.",
|
||||
@@ -1107,6 +1136,7 @@
|
||||
"developer_documentation": "Documentação para programadores",
|
||||
"get_in_touch": "Entre em contacto",
|
||||
"contact_support": "Contacte o suporte",
|
||||
"premium_support": "Suporte Premium",
|
||||
"community_support": "Apoio da Comunidade",
|
||||
"feedback": "Feedback",
|
||||
"submitted_feedback": "Obrigado pelo seu feedback!",
|
||||
@@ -1270,6 +1300,7 @@
|
||||
"default_calendar_selected": "Calendário predefinido",
|
||||
"hide_from_profile": "Ocultar do perfil",
|
||||
"event_setup_tab_title": "Configuração do evento",
|
||||
"availability_not_found_in_schedule_error": "Não foi encontrada nenhuma disponibilidade na agenda",
|
||||
"event_limit_tab_title": "Limites",
|
||||
"event_limit_tab_description": "Com que frequência pode ser reservado",
|
||||
"event_advanced_tab_description": "Definições do calendário e muito mais...",
|
||||
@@ -1290,6 +1321,7 @@
|
||||
"do_this": "Fazer isto",
|
||||
"turn_off": "Desligar",
|
||||
"turn_on": "Ativar",
|
||||
"cancelled_bookings_cannot_be_rescheduled": "As reservas canceladas não podem ser reagendadas",
|
||||
"settings_updated_successfully": "Definições atualizadas com sucesso",
|
||||
"error_updating_settings": "Erro ao atualizar as definições",
|
||||
"personal_cal_url": "O meu endereço pessoal do {{appName}}",
|
||||
@@ -1313,6 +1345,7 @@
|
||||
"customize_your_brand_colors": "Personalize a cor da sua marca na sua página de reservas.",
|
||||
"pro": "Pro",
|
||||
"removes_cal_branding": "Remove qualquer identificação de marca relacionada o {{appName}}, por exemplo, 'Com tecnologia {{appName}}.'",
|
||||
"instant_meeting_with_title": "Reunião instantânea com {{name}}",
|
||||
"profile_picture": "Imagem do perfil",
|
||||
"upload": "Carregar",
|
||||
"add_profile_photo": "Adicionar imagem de perfil",
|
||||
@@ -1368,6 +1401,7 @@
|
||||
"event_name_info": "O nome do tipo de evento",
|
||||
"event_date_info": "A data do evento",
|
||||
"event_time_info": "A hora de início do evento",
|
||||
"event_type_not_found": "Tipo de evento não encontrado",
|
||||
"location_variable": "Localização",
|
||||
"location_info": "O local do evento",
|
||||
"additional_notes_variable": "Notas Adicionais",
|
||||
@@ -1410,6 +1444,7 @@
|
||||
"slot_length": "Comprimento do espaço",
|
||||
"booking_appearance": "Aparência da reserva",
|
||||
"appearance_team_description": "Faça a gestão das definições de aparência da reserva da sua equipa",
|
||||
"appearance_org_description": "Faça a gestão das definições de aparência da reserva da sua organização",
|
||||
"only_owner_change": "Só o proprietário desta equipa pode fazer alterações na reserva da equipa ",
|
||||
"team_disable_cal_branding_description": "Remove qualquer identificação de marca relacionada o {{appName}}, por exemplo, 'Com tecnologia {{appName}}'",
|
||||
"invited_by_team": "{{teamName}} enviou um convite para se juntar à sua equipa como {{role}}",
|
||||
@@ -1463,6 +1498,7 @@
|
||||
"password_updated": "Palavra-passe atualizada!",
|
||||
"pending_payment": "Pagamento pendente",
|
||||
"pending_invites": "Convites pendentes",
|
||||
"pending_organization_invites": "Convites de organização pendentes",
|
||||
"not_on_cal": "Não existe em {{appName}}",
|
||||
"no_calendar_installed": "Nenhum calendário instalado",
|
||||
"no_calendar_installed_description": "Ainda não associou nenhum dos seus calendários",
|
||||
@@ -1474,6 +1510,8 @@
|
||||
"report_app": "Reportar aplicação",
|
||||
"limit_booking_frequency": "Limitar a frequência das reservas",
|
||||
"limit_booking_frequency_description": "Limitar o número de vezes que este evento pode ser reservado",
|
||||
"limit_booking_only_first_slot": "Limitar a reserva apenas à primeira vaga",
|
||||
"limit_booking_only_first_slot_description": "Permitir que apenas a primeira vaga de todos os dias seja reservada",
|
||||
"limit_total_booking_duration": "Limitar a duração total da reserva",
|
||||
"limit_total_booking_duration_description": "Limitar o tempo total que pode ser reservado para este evento",
|
||||
"add_limit": "Adicionar limite",
|
||||
@@ -1541,10 +1579,14 @@
|
||||
"your_org_disbanded_successfully": "A sua organização foi dissolvida com sucesso",
|
||||
"error_creating_team": "Erro ao criar a equipa",
|
||||
"you": "Você",
|
||||
"or_continue_with": "Ou continuar com",
|
||||
"resend_email": "Reenviar e-mail",
|
||||
"member_already_invited": "O membro já foi convidado",
|
||||
"already_in_use_error": "O nome de utilizador já está a ser utilizado",
|
||||
"enter_email_or_username": "Especifique um e-mail ou nome de utilizador",
|
||||
"enter_email": "Inserir um e-mail",
|
||||
"enter_emails": "Inserir e-mails",
|
||||
"too_many_invites": "Está limitado a convidar um máximo de {{nbUsers}} utilizadores de cada vez.",
|
||||
"team_name_taken": "Este nome já está a ser utilizado",
|
||||
"must_enter_team_name": "Deve especificar um nome para a equipa",
|
||||
"team_url_required": "Deve especificar um endereço para a equipa",
|
||||
@@ -1624,6 +1666,7 @@
|
||||
"individual": "Individual",
|
||||
"all_bookings_filter_label": "Todas as reservas",
|
||||
"all_users_filter_label": "Todos os utilizadores",
|
||||
"all_event_types_filter_label": "Todos os tipos de evento",
|
||||
"your_bookings_filter_label": "As suas reservas",
|
||||
"meeting_url_variable": "URL da reunião",
|
||||
"meeting_url_info": "URL da reunião de conferência",
|
||||
@@ -1720,9 +1763,11 @@
|
||||
"organizer_timezone": "Fuso horário do organizador",
|
||||
"email_user_cta": "Ver convite",
|
||||
"email_no_user_invite_heading_team": "Recebeu um convite para fazer parte de uma equipa {{appName}}",
|
||||
"email_no_user_invite_heading_subteam": "Recebeu um convite para fazer parte de uma equipa da organização {{parentTeamName}}",
|
||||
"email_no_user_invite_heading_org": "Recebeu um convite para fazer parte de uma organização {{appName}}",
|
||||
"email_no_user_invite_subheading": "Recebeu um convite de {{invitedBy}} para fazer parte da respetiva equipa em {{appName}}. {{appName}} é um ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua equipa agendar reuniões sem o pingue-pongue de e-mails.",
|
||||
"email_user_invite_subheading_team": "Recebeu um convite de {{invitedBy}} para fazer parte da equipa `{{teamName}}` em {{appName}}. {{appName}} é uma ferramenta de conciliação e agendamento de eventos que lhe permite a si e à sua equipa agendar reuniões sem o pingue-pongue de mensagens eletrónicas.",
|
||||
"email_user_invite_subheading_subteam": "Recebeu um convite de {{invitedBy}} para fazer parte da equipa {{teamName}} da organização {{parentTeamName}} em {{appName}}. {{appName}} é uma ferramenta de conciliação e agendamento de eventos que lhe permite, a si e à sua equipa, agendar reuniões sem o pingue-pongue de mensagens eletrónicas.",
|
||||
"email_user_invite_subheading_org": "Recebeu um convite de {{invitedBy}} para fazer parte de {{teamName}} da sua organização em {{appName}}. {{appName}} é a solução de agendamento de eventos que lhe permite a si e à sua organização agendar reuniões sem o pingue-pongue de e-mails.",
|
||||
"email_no_user_invite_steps_intro": "Vamos ajudar com alguns pequenos passos para que você e a sua {{entity}} possam desfrutar rapidamente de uma gestão de agendamentos sem complicações.",
|
||||
"email_no_user_step_one": "Escolha o seu nome de utilizador",
|
||||
@@ -1855,7 +1900,9 @@
|
||||
"review_event_type": "Rever Tipo de Evento",
|
||||
"looking_for_more_analytics": "À procura de mais análises?",
|
||||
"looking_for_more_insights": "À procura de mais Insights?",
|
||||
"filters": "Filtros",
|
||||
"add_filter": "Adicionar filtro",
|
||||
"remove_filters": "Limpar todos os filtros",
|
||||
"select_user": "Selecionar utilizador",
|
||||
"select_event_type": "Selecionar tipo de evento",
|
||||
"select_date_range": "Selecionar intervalo de datas",
|
||||
@@ -1946,6 +1993,7 @@
|
||||
"connect_google_workspace": "Associar o Google Workspace",
|
||||
"google_workspace_admin_tooltip": "Tem de ser um Administrador do Workspace para utilizar esta funcionalidade",
|
||||
"first_event_type_webhook_description": "Crie o seu primeiro webhook para este tipo de evento",
|
||||
"create_instant_meeting_webhook_description": "Crie o seu primeiro webhook com 'Reunião instantânea criada' como acionador para este tipo de evento",
|
||||
"install_app_on": "Instalar aplicação em",
|
||||
"create_for": "Criar para",
|
||||
"currency": "Moeda",
|
||||
@@ -2010,6 +2058,8 @@
|
||||
"add_times_to_your_email": "Selecione alguns horários disponíveis e incorpore-os no seu E-mail",
|
||||
"select_time": "Selecionar horário",
|
||||
"select_date": "Selecionar data",
|
||||
"connecting_you_to_someone": "Estamos a conectá-lo a alguém.",
|
||||
"please_do_not_close_this_tab": "Não feche este separador",
|
||||
"see_all_available_times": "Ver todos os horários disponíveis",
|
||||
"org_team_names_example_1": "Por exemplo, Equipa de Marketing",
|
||||
"org_team_names_example_2": "Por exemplo, Equipa de Vendas",
|
||||
@@ -2027,8 +2077,12 @@
|
||||
"description_requires_booker_email_verification": "Para garantir a confirmação do e-mail do responsável pela reserva antes de agendar eventos",
|
||||
"requires_confirmation_mandatory": "As mensagens de texto só podem ser enviadas aos participantes quando o tipo de evento requerer confirmação.",
|
||||
"organizations": "Organizações",
|
||||
"upload_cal_video_logo": "Carregar logótipo de Cal Video",
|
||||
"update_cal_video_logo": "Atualizar logótipo de Cal Video",
|
||||
"cal_video_logo_upload_instruction": "Para garantir que o seu logótipo seja visível contra o fundo escuro do Cal Video, carregue uma imagem de cores claras em formato PNG ou SVG, para manter a transparência.",
|
||||
"org_admin_other_teams": "Outras equipas",
|
||||
"org_admin_other_teams_description": "Aqui pode ver equipas dentro da sua organização e das quais não faz parte. Pode adicionar-se às mesmas, se necessário.",
|
||||
"not_part_of_org": "Você não faz parte de nenhuma organização",
|
||||
"no_other_teams_found": "Não foram encontradas outras equipas",
|
||||
"no_other_teams_found_description": "Não há outras equipas nesta organização.",
|
||||
"attendee_first_name_variable": "Primeiro nome do participante",
|
||||
@@ -2064,7 +2118,9 @@
|
||||
"org_error_processing": "Ocorreu um erro ao processar esta organização",
|
||||
"orgs_page_description": "Uma lista de todas as organizações. Aceitar uma organização permitirá que todos os utilizadores com esse domínio de e-mail se registem SEM a verificação de e-mail.",
|
||||
"unverified": "Não verificada",
|
||||
"verified": "Confirmado",
|
||||
"dns_missing": "DNS em falta",
|
||||
"dns_configured": "DNS configurado",
|
||||
"mark_dns_configured": "Marcar como o DNS configurado",
|
||||
"value": "Valor",
|
||||
"your_organization_updated_sucessfully": "A sua organização foi atualizada com sucesso",
|
||||
@@ -2074,8 +2130,11 @@
|
||||
"oAuth": "OAuth",
|
||||
"recently_added": "Adicionados recentemente",
|
||||
"connect_all_calendars": "Associe todos os seus calendários",
|
||||
"connect_all_calendars_description": "{{appName}} lê a disponibilidade de todos os seus calendários existentes.",
|
||||
"workflow_automation": "Automação de fluxo de trabalho",
|
||||
"workflow_automation_description": "Personalize a sua experiência de agendamento com fluxos de trabalho",
|
||||
"scheduling_for_your_team": "Automação de fluxo de trabalho",
|
||||
"scheduling_for_your_team_description": "Faça o agendamento para a sua equipa, com agendamento coletivo e distribuição equilibrada",
|
||||
"no_members_found": "Nenhum membro encontrado",
|
||||
"event_setup_length_error": "Configuração do Evento: a duração deve ser de, pelo menos, 1 minuto.",
|
||||
"availability_schedules": "Horários de Disponibilidade",
|
||||
@@ -2108,10 +2167,100 @@
|
||||
"overlay_my_calendar": "Sobrepor o meu calendário",
|
||||
"overlay_my_calendar_toc": "Ao ligar-se ao seu calendário, você aceita a nossa política de privacidade e termos de utilização. Pode revogar o acesso a qualquer momento.",
|
||||
"view_overlay_calendar_events": "Consulte os eventos do seu calendário para evitar agendamentos conflituantes.",
|
||||
"join_event_location": "Junte-se a {{eventLocationType}}",
|
||||
"troubleshooting": "Resolução de problemas",
|
||||
"calendars_were_checking_for_conflicts": "Calendários que estamos a verificar por conflitos",
|
||||
"availabilty_schedules": "Horários de disponibilidade",
|
||||
"manage_calendars": "Gerir calendários",
|
||||
"manage_availability_schedules": "Gerir horários de disponibilidade",
|
||||
"lock_timezone_toggle_on_booking_page": "Bloquear fuso horário na página de reserva",
|
||||
"description_lock_timezone_toggle_on_booking_page": "Para bloquear o fuso horário na página de reservas. Útil para eventos presenciais.",
|
||||
"event_setup_multiple_payment_apps_error": "Só pode ter uma aplicação de pagamentos ativada por tipo de evento.",
|
||||
"number_in_international_format": "Insira o número no formato internacional.",
|
||||
"install_calendar": "Instalar Calendário",
|
||||
"branded_subdomain": "Subdomínio de marca",
|
||||
"branded_subdomain_description": "Obtenha o seu próprio subdomínio de marca. Por exemplo, acme.cal.com",
|
||||
"org_insights": "Insights de toda a organização",
|
||||
"org_insights_description": "Compreenda como toda a sua organização está a utilizar o tempo",
|
||||
"extensive_whitelabeling": "Apoio dedicado à integração e engenharia",
|
||||
"extensive_whitelabeling_description": "Personalize a sua experiência de agendamento com o seu próprio logótipo, cores e muito mais",
|
||||
"unlimited_teams": "Equipas ilimitadas",
|
||||
"unlimited_teams_description": "Adicione à sua organização tantas subequipas quanto for necessário",
|
||||
"unified_billing": "Faturação unificada",
|
||||
"unified_billing_description": "Adicione um único cartão de crédito para pagar todas as subscrições da sua equipa",
|
||||
"advanced_managed_events": "Tipos de evento geridos avançados",
|
||||
"advanced_managed_events_description": "Adicione um único cartão de crédito para pagar todas as subscrições da sua equipa",
|
||||
"enterprise_description": "Atualize para Enterprise para criar a sua Organização",
|
||||
"create_your_org": "Crie a sua Organização",
|
||||
"create_your_org_description": "Atualize para Enterprise e receba um subdomínio, faturação unificada, Insights, personalização abrangente da imagem corporativa e muito mais",
|
||||
"other_payment_app_enabled": "Só pode ativar uma aplicação de pagamento por tipo de evento",
|
||||
"admin_delete_organization_description": "<ul><li>As equipas que são membros desta organização também serão eliminadas, em conjunto com os respetivos tipos de evento</li><li>Utilizadores que faziam parte da organização não serão eliminados e os seus tipos de evento também permanecerão intactos.</li><li>Os nomes de utilizador serão alterados para permitir que possam existir fora da organização</li></ul>",
|
||||
"admin_delete_organization_title": "Eliminar {{organizationName}}?",
|
||||
"published": "Publicada",
|
||||
"unpublished": "Não publicada",
|
||||
"publish": "Publicar",
|
||||
"org_publish_error": "Não foi possível publicar a organização",
|
||||
"troubleshooter_tooltip": "Abra o solucionador de problemas e descubra o que está errado com o seu agendamento",
|
||||
"need_help": "Precisa de ajuda?",
|
||||
"troubleshooter": "Solucionador de problemas",
|
||||
"please_install_a_calendar": "Por favor, instale um calendário",
|
||||
"instant_tab_title": "Reserva imediata",
|
||||
"instant_event_tab_description": "Permita que as pessoas reservem imediatamente",
|
||||
"uprade_to_create_instant_bookings": "Atualize para Enterprise e permita que os convidados se juntem a uma chamada instantânea em que os participantes podem entrar diretamente. Isto é apenas para tipos de evento de equipa",
|
||||
"dont_want_to_wait": "Não quer esperar?",
|
||||
"meeting_started": "Reunião iniciada",
|
||||
"pay_and_book": "Pagar para reservar",
|
||||
"booking_not_found_error": "Não foi possível encontrar a reserva",
|
||||
"booking_seats_full_error": "Os lugares para reserva estão cheios",
|
||||
"missing_payment_credential_error": "Credenciais de pagamento em falta",
|
||||
"missing_payment_app_id_error": "ID da aplicação de pagamento em falta",
|
||||
"not_enough_available_seats_error": "A reserva não tem lugares disponíveis suficientes",
|
||||
"user_redirect_title": "{{username}} está ausente por um breve período de tempo.",
|
||||
"user_redirect_description": "Entretanto, {{profile.username}} ficará responsável por todas as novas reuniões agendadas em nome de {{username}}.",
|
||||
"out_of_office": "Fora do escritório",
|
||||
"out_of_office_description": "Configure as ações no seu perfil enquanto estiver ausente.",
|
||||
"send_request": "Enviar pedido",
|
||||
"start_date_and_end_date_required": "Data de início e data de fim são necessárias",
|
||||
"start_date_must_be_before_end_date": "Data de início deve ser anterior à data de fim",
|
||||
"start_date_must_be_in_the_future": "Data de início deve ser no futuro",
|
||||
"user_not_found": "Utilizador não encontrado",
|
||||
"out_of_office_entry_already_exists": "Já existe uma entrada de fora do escritório",
|
||||
"out_of_office_id_required": "É necessária a ID da entrada de fora do escritório",
|
||||
"booking_redirect_infinite_not_allowed": "Já existe um redirecionamento de reservas desse utilizador para si.",
|
||||
"success_entry_created": "Criou uma nova entrada com sucesso",
|
||||
"booking_redirect_email_subject": "Notificação de redirecionamento de reserva",
|
||||
"booking_redirect_email_title": "Notificação de redirecionamento de reserva",
|
||||
"booking_redirect_email_description": "Recebeu um redirecionamento de reservas de {{toName}}, pelo que as ligações do respetivo perfil serão redirecionados para o seu perfil durante o intervalo de tempo: ",
|
||||
"success_accept_booking_redirect": "Aceitou este pedido de redirecionamento de reservas.",
|
||||
"success_reject_booking_redirect": "Rejeitou este pedido de redirecionamento de reservas.",
|
||||
"copy_link_booking_redirect_request": "Copiar ligação para partilhar o pedido",
|
||||
"booking_redirect_request_title": "Pedido de redirecionamento de reservas",
|
||||
"select_team_member": "Selecionar o membro da equipa",
|
||||
"going_away_title": "Vai ausentar-se? Simplesmente marque a ligação do seu perfil como indisponível por um determinado período de tempo.",
|
||||
"redirect_team_enabled": "Redirecionar o seu perfil para outro membro da equipa",
|
||||
"redirect_team_disabled": "Redirecionar o seu perfil para outro membro da equipa (é necessário um plano Team)",
|
||||
"out_of_office_unavailable_list": "Lista de indisponibilidades de fora do escritório",
|
||||
"success_deleted_entry_out_of_office": "Entrada eliminada com sucesso",
|
||||
"temporarily_out_of_office": "Temporariamente fora do escritório?",
|
||||
"add_a_redirect": "Adicionar um redirecionamento",
|
||||
"create_entry": "Criar entrada",
|
||||
"time_range": "Intervalo de tempo",
|
||||
"automatically_add_all_team_members": "Adicionar todos os membros da equipa, incluindo membros futuros",
|
||||
"redirect_to": "Redirecionar para",
|
||||
"having_trouble_finding_time": "Com dificuldades em encontrar um horário?",
|
||||
"show_more": "Mostrar mais",
|
||||
"assignment_description": "Agende reuniões quando todos estiverem disponíveis ou rode entre os membros da sua equipa",
|
||||
"lowest": "a mais baixa",
|
||||
"low": "baixa",
|
||||
"medium": "média",
|
||||
"high": "alta",
|
||||
"Highest": "a mais alta",
|
||||
"send_booker_to": "Enviar responsável pela reserva para",
|
||||
"set_priority": "Definir prioridade",
|
||||
"priority_for_user": "Prioridade para {{userName}}",
|
||||
"change_priority": "alterar prioridade",
|
||||
"field_identifiers_as_variables": "Utilize identificadores de campo como variáveis para o seu redirecionamento de evento personalizado",
|
||||
"field_identifiers_as_variables_with_example": "Utilize identificadores de campo como variáveis para o seu redirecionamento de evento personalizado (ex.: {{variable}})",
|
||||
"account_already_linked": "A conta já está vinculada",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -13,10 +13,17 @@
|
||||
"reset_password_subject": "{{appName}}: інструкції зі скидання пароля",
|
||||
"verify_email_subject": "{{appName}}: підтвердьте свій обліковий запис",
|
||||
"check_your_email": "Перевірте свою електронну пошту",
|
||||
"old_email_address": "Стара адреса електронної пошти",
|
||||
"new_email_address": "Нова адреса електронної пошти",
|
||||
"verify_email_page_body": "Ми надіслали лист на адресу {{email}}. Підтвердьте свою електронну адресу, щоб уникнути проблем із доставкою листів і даних календаря із застосунку {{appName}}.",
|
||||
"verify_email_banner_body": "Підтвердьте свою електронну адресу, щоб уникнути проблем із доставкою листів і даних календаря",
|
||||
"verify_email_email_header": "Підтвердьте свою електронну адресу",
|
||||
"verify_email_email_button": "Підтвердити ел. адресу",
|
||||
"verify_email_change_description": "Ви нещодавно просили змінити адресу електронної пошти, яку ви використовуєте для входу в обліковий запис {{appName}}. Будь ласка, натисніть на кнопку нижче, щоб підтвердити нову електронну адресу.",
|
||||
"verify_email_change_success_toast": "Оновлено вашу електронну пошту до {{email}}",
|
||||
"verify_email_change_failure_toast": "Не вдалося оновити адресу електронної пошти.",
|
||||
"change_of_email": "Підтвердьте вашу нову адресу електронної пошти для {{appName}}",
|
||||
"change_of_email_toast": "Ми надіслали посилання для підтвердження на {{email}}. Ми оновимо адресу електронної пошти після натискання цього посилання.",
|
||||
"copy_somewhere_safe": "Збережіть цей ключ API у надійному місці. Ви не зможете знову його переглянути.",
|
||||
"verify_email_email_body": "Підтвердьте свою електронну адресу, натиснувши кнопку нижче.",
|
||||
"verify_email_by_code_email_body": "Підтвердьте свою електронну адресу, використавши нижчезазначений код.",
|
||||
@@ -56,6 +63,17 @@
|
||||
"a_refund_failed": "Не вдалося повернути кошти",
|
||||
"awaiting_payment_subject": "Очікування платежу: {{title}}, {{date}}",
|
||||
"meeting_awaiting_payment": "Ваша нарада очікує на оплату",
|
||||
"dark_theme_contrast_error": "Колір темної теми не проходить перевірку контрасту. Ми рекомендуємо змінити цей колір, щоб його було краще бачити.",
|
||||
"light_theme_contrast_error": "Колір темної теми не проходить перевірку контрасту. Ми рекомендуємо змінити цей колір, щоб його було краще бачити.",
|
||||
"payment_not_created_error": "Не вдалося створити платіж",
|
||||
"couldnt_charge_card_error": "Не вдалося отримати оплату з картки",
|
||||
"no_available_users_found_error": "Не знайдено жодного доступного користувача. Чи могли б ви спробувати інший часовий слот?",
|
||||
"request_body_end_time_internal_error": "Внутрішня помилка. Тіло запиту не містить кінцевого часу",
|
||||
"create_calendar_event_error": "Не вдалося створити подію у календарі організаторів",
|
||||
"update_calendar_event_error": "Неможливо оновити подію Календаря.",
|
||||
"delete_calendar_event_error": "Неможливо оновити подію Календаря.",
|
||||
"already_signed_up_for_this_booking_error": "Ви вже зареєстровані в цьому бронювання.",
|
||||
"hosts_unavailable_for_booking": "Деякі хости недоступні для бронювання.",
|
||||
"help": "Довідка",
|
||||
"price": "Ціна",
|
||||
"paid": "Сплачено",
|
||||
@@ -101,6 +119,7 @@
|
||||
"requested_to_reschedule_subject_attendee": "Потрібно перенести бронювання: забронюйте новий час для заходу «{{eventType}}» із користувачем {{name}}",
|
||||
"hi_user_name": "Привіт, {{name}}!",
|
||||
"ics_event_title": "{{eventType}} «{{name}}»",
|
||||
"please_book_a_time_sometime_later": "Ой, ми не можемо підключитися до вас цього разу. Будь ласка, організуйте майбутній дзвінок замість цього",
|
||||
"new_event_subject": "Новий захід: {{attendeeName}} – {{date}} – {{eventType}}",
|
||||
"join_by_entrypoint": "Приєднатися через {{entryPoint}}",
|
||||
"notes": "Примітки",
|
||||
@@ -117,15 +136,20 @@
|
||||
"meeting_id": "Ідентифікатор наради",
|
||||
"meeting_password": "Пароль наради",
|
||||
"meeting_url": "URL-адреса наради",
|
||||
"meeting_url_not_found": "Посилання на зустріч не знайдено",
|
||||
"token_not_found": "Токен не знайдено",
|
||||
"some_other_host_already_accepted_the_meeting": "Деякі інші хости вже прийняли зустріч. Ви все ще хочете приєднатися? <1>Продовжити зустріч</1>",
|
||||
"meeting_request_rejected": "Ваш запит на нараду відхилено",
|
||||
"rejected_event_type_with_organizer": "Відхилено: {{eventType}} «{{organizer}}», {{date}}",
|
||||
"hi": "Привіт",
|
||||
"join_team": "Увійти в команду",
|
||||
"manage_this_team": "Керуйте цією командою",
|
||||
"team_info": "Про команду",
|
||||
"join_meeting": "Приєднатися до зустрічі",
|
||||
"request_another_invitation_email": "Якщо ви не хочете використовувати {{toEmail}} в {{appName}} або вже маєте обліковий запис {{appName}}, надішліть запит на запрошення на потрібну електронну адресу.",
|
||||
"you_have_been_invited": "Вас запрошено приєднатися до команди «{{teamName}}»",
|
||||
"user_invited_you": "{{user}} запрошує вас приєднатись: {{entity}} «{{team}}» в {{appName}}",
|
||||
"user_invited_you_to_subteam": "{{user}} запросив вас приєднатися до команди {{team}} організації {{parentTeamName}} {{appName}}",
|
||||
"hidden_team_member_title": "У цій команді вас приховано",
|
||||
"hidden_team_member_message": "Ваше місце не оплачено. Перейдіть на версію Pro або попросіть власника команди оплатити ваше місце.",
|
||||
"hidden_team_owner_message": "Щоб користуватися функціями команди, потрібен обліковий запис версії Pro. До переходу на цю версію вас буде приховано.",
|
||||
@@ -229,6 +253,7 @@
|
||||
"reset_your_password": "Установіть новий пароль, дотримуючись інструкцій, надісланих на вашу електронну адресу.",
|
||||
"email_change": "Увійдіть у систему за допомогою нової електронної адреси й пароля.",
|
||||
"create_your_account": "Створіть власний обліковий запис",
|
||||
"create_your_calcom_account": "Створіть обліковий запис Cal.com",
|
||||
"sign_up": "Реєстрація",
|
||||
"youve_been_logged_out": "Ви вийшли із системи",
|
||||
"hope_to_see_you_soon": "Чекатимемо на ваше повернення!",
|
||||
@@ -266,6 +291,9 @@
|
||||
"nearly_there_instructions": "І останнє: стислі відомості про вас і ваше фото заохочуватимуть інших бронюванювати ваш час, адже вони бачитимуть, з ким домовляються.",
|
||||
"set_availability_instructions": "Визначте повторювані діапазони часу, у які ви доступні. Ви зможете додати більше таких діапазонів пізніше та призначити їх різним календарям.",
|
||||
"set_availability": "Укажіть, коли ви доступні",
|
||||
"set_availbility_description": "Встановіть розклад за яким ви хочете щоб вас бронювати.",
|
||||
"share_a_link_or_embed": "Поділитись посиланням або вбудованим вмістом",
|
||||
"share_a_link_or_embed_description": "Поділитись вашим {{appName}} посиланням або вбудуйте його на сайті.",
|
||||
"availability_settings": "Налаштування доступності",
|
||||
"continue_without_calendar": "Продовжити без календаря",
|
||||
"continue_with": "Продовжити з {{appName}}",
|
||||
@@ -281,6 +309,7 @@
|
||||
"welcome_to_calcom": "Вітаємо в {{appName}}",
|
||||
"welcome_instructions": "Скажіть, як вас звати та в якому ви часовому поясі. Ці дані можна буде змінити пізніше.",
|
||||
"connect_caldav": "Підключитися до CalDav (бета)",
|
||||
"connect_ics_feed": "Підключитися до ICS стрічки",
|
||||
"connect": "Підключитися",
|
||||
"try_for_free": "Спробувати безкоштовно",
|
||||
"create_booking_link_with_calcom": "Створіть власне посилання на бронювання в {{appName}}",
|
||||
@@ -419,6 +448,7 @@
|
||||
"browse_api_documentation": "Переглянути нашу документацію з API",
|
||||
"leverage_our_api": "Використовуйте наш інтерфейс API, щоб отримати повний контроль і можливості налаштування.",
|
||||
"create_webhook": "Створити вебгук",
|
||||
"instant_meeting": "Миттєву зустріч створено",
|
||||
"booking_cancelled": "Бронювання скасовано",
|
||||
"booking_rescheduled": "Бронювання перенесено",
|
||||
"recording_ready": "Посилання для завантаження запису готове",
|
||||
@@ -533,6 +563,7 @@
|
||||
"enter_number_between_range": "Введіть число від 1 до {{maxOccurences}}",
|
||||
"email_address": "Електронна адреса",
|
||||
"enter_valid_email": "Введіть дійсну адресу електронної пошти",
|
||||
"please_schedule_future_call": " Якщо ви не доступні через {{seconds}} секунд, будь ласка, сплануйте новий дзвінок",
|
||||
"location": "Розташування",
|
||||
"address": "Адреса",
|
||||
"enter_address": "Введіть адресу",
|
||||
@@ -592,6 +623,7 @@
|
||||
"new_member": "Новий учасник",
|
||||
"invite": "Запросити",
|
||||
"add_team_members": "Додати учасників команди",
|
||||
"add_org_members": "Додати учасників",
|
||||
"add_team_members_description": "Запросіть когось у свою команду",
|
||||
"add_team_member": "Додати учасника команди",
|
||||
"invite_new_member": "Запросити нового учасника команди",
|
||||
|
||||
@@ -1447,6 +1447,8 @@
|
||||
"copy_link_to_form": "将链接复制到表格",
|
||||
"theme": "主题",
|
||||
"theme_applies_note": "这仅适用于您的公开预约页面",
|
||||
"app_theme": "仪表板主题",
|
||||
"app_theme_applies_note": "这仅在您登录仪表板时适用",
|
||||
"theme_system": "系统默认设置",
|
||||
"add_a_team": "添加团队",
|
||||
"add_webhook_description": "当 {{appName}} 上有活动时实时接收会议数据",
|
||||
|
||||
Executable
+47
@@ -0,0 +1,47 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Only deploy to production
|
||||
if [ "$VERCEL_ENV" != "production" ]; then exit 0; fi
|
||||
|
||||
checkRoute () {
|
||||
if [ "$1" != '1' ]; then rm -rf $2; fi
|
||||
}
|
||||
|
||||
# These conditionals are used to remove directories from the build that are not needed in production
|
||||
# This is to reduce the size of the build and prevent OOM errors
|
||||
checkRoute "$APP_ROUTER_EVENT_TYPES_ENABLED" app/future/event-types
|
||||
checkRoute "$APP_ROUTER_SETTINGS_ADMIN_ENABLED" app/future/settings/admin
|
||||
checkRoute "$APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED" app/future/apps/installed
|
||||
checkRoute "$APP_ROUTER_APPS_SLUG_ENABLED" app/future/apps/\[slug\]
|
||||
checkRoute "$APP_ROUTER_APPS_SLUG_SETUP_ENABLED" app/future/apps/\[slug\]/setup
|
||||
checkRoute "$APP_ROUTER_APPS_CATEGORIES_ENABLED" app/future/apps/categories
|
||||
checkRoute "$APP_ROUTER_APPS_CATEGORIES_CATEGORY_ENABLED" app/future/apps/categories/\[category\]
|
||||
checkRoute "$APP_ROUTER_WORKFLOWS_ENABLED" app/future/workflows
|
||||
checkRoute "$APP_ROUTER_SETTINGS_TEAMS_ENABLED" app/future/settings/teams
|
||||
checkRoute "$APP_ROUTER_GETTING_STARTED_STEP_ENABLED" app/future/getting-started
|
||||
checkRoute "$APP_ROUTER_APPS_ENABLED" app/future/apps
|
||||
checkRoute "$APP_ROUTER_BOOKINGS_STATUS_ENABLED" app/future/bookings
|
||||
checkRoute "$APP_ROUTER_VIDEO_ENABLED" app/future/video
|
||||
checkRoute "$APP_ROUTER_TEAMS_ENABLED" app/future/teams
|
||||
|
||||
# These are routes that don't have and environment variable to enable or disable them
|
||||
# Will stop removing gradually as we test and confirm that they are working
|
||||
rm -rf \
|
||||
app/future/[user]\
|
||||
app/future/auth\
|
||||
app/future/booking\
|
||||
app/future/connect-and-join\
|
||||
app/future/d\
|
||||
app/future/enterprise\
|
||||
app/future/insights\
|
||||
app/future/maintenance\
|
||||
app/future/more\
|
||||
app/future/org\
|
||||
app/future/payment\
|
||||
app/future/reschedule\
|
||||
app/future/routing-forms\
|
||||
app/future/settings\
|
||||
app/future/signup\
|
||||
app/future/team
|
||||
|
||||
exit 1
|
||||
@@ -13,11 +13,15 @@ import {
|
||||
getDate,
|
||||
getGoogleCalendarCredential,
|
||||
createBookingScenario,
|
||||
createOrganization,
|
||||
getOrganizer,
|
||||
getScenarioData,
|
||||
} from "../utils/bookingScenario/bookingScenario";
|
||||
|
||||
vi.mock("@calcom/lib/constants", () => ({
|
||||
IS_PRODUCTION: true,
|
||||
WEBAPP_URL: "http://localhost:3000",
|
||||
RESERVED_SUBDOMAINS: ["auth", "docs"],
|
||||
}));
|
||||
|
||||
declare global {
|
||||
@@ -1255,5 +1259,73 @@ describe("getSchedule", () => {
|
||||
{ dateString: plus3DateString }
|
||||
);
|
||||
});
|
||||
|
||||
test("getSchedule can get slots of org's member event type when orgSlug, eventTypeSlug passed as input", async () => {
|
||||
const org = await createOrganization({ name: "acme", slug: "acme" });
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
// So, that it picks the first schedule from the list
|
||||
defaultScheduleId: null,
|
||||
organizationId: org.id,
|
||||
// Has morning shift with some overlap with morning shift
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
});
|
||||
|
||||
const scenario = await createBookingScenario(
|
||||
getScenarioData(
|
||||
{
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 45,
|
||||
length: 45,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
},
|
||||
{ id: org.id }
|
||||
)
|
||||
);
|
||||
|
||||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
|
||||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
|
||||
|
||||
const getScheduleRes = await getSchedule({
|
||||
input: {
|
||||
eventTypeSlug: scenario.eventTypes[0]?.slug,
|
||||
startTime: `${plus1DateString}T18:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T18:29:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
orgSlug: "acme",
|
||||
usernameList: [organizer.username],
|
||||
},
|
||||
});
|
||||
|
||||
expect(getScheduleRes).toHaveTimeSlots(
|
||||
[
|
||||
`04:00:00.000Z`,
|
||||
`04:45:00.000Z`,
|
||||
`05:30:00.000Z`,
|
||||
`06:15:00.000Z`,
|
||||
`07:00:00.000Z`,
|
||||
`07:45:00.000Z`,
|
||||
`08:30:00.000Z`,
|
||||
`09:15:00.000Z`,
|
||||
`10:00:00.000Z`,
|
||||
`10:45:00.000Z`,
|
||||
`11:30:00.000Z`,
|
||||
],
|
||||
{ dateString: plus2DateString }
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,6 +42,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -62,6 +63,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -88,6 +90,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual(undefined);
|
||||
expect(result.oldUserIds).toEqual(undefined);
|
||||
@@ -109,6 +112,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -125,6 +129,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -158,6 +163,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -173,13 +179,14 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {
|
||||
bookingLimits: undefined,
|
||||
},
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
hidden: false,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
hashedLink: { create: { link: expect.any(String) } },
|
||||
},
|
||||
where: {
|
||||
@@ -207,6 +214,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(result.newUserIds).toEqual([]);
|
||||
expect(result.oldUserIds).toEqual([]);
|
||||
@@ -232,6 +240,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
// Have been called
|
||||
expect(result.newUserIds).toEqual([5]);
|
||||
@@ -253,6 +262,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
id: 123,
|
||||
@@ -270,6 +280,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -302,6 +313,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -318,14 +330,15 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {
|
||||
length: 30,
|
||||
},
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
scheduleId: undefined,
|
||||
hidden: false,
|
||||
length: 30,
|
||||
hashedLink: undefined,
|
||||
},
|
||||
where: {
|
||||
userId_parentId: {
|
||||
@@ -355,6 +368,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
requiresBookerEmailVerification,
|
||||
lockTimeZoneToggleOnBookingPage,
|
||||
useEventTypeDestinationCalendarEmail,
|
||||
secondaryEmailId,
|
||||
...evType
|
||||
} = mockFindFirstEventType({
|
||||
metadata: { managedEventConfig: {} },
|
||||
@@ -379,6 +393,7 @@ describe("handleChildrenEventTypes", () => {
|
||||
connectedLink: null,
|
||||
prisma: prismaMock,
|
||||
profileId: null,
|
||||
updatedValues: {},
|
||||
});
|
||||
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
|
||||
data: {
|
||||
@@ -404,13 +419,8 @@ describe("handleChildrenEventTypes", () => {
|
||||
});
|
||||
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
|
||||
data: {
|
||||
...evType,
|
||||
bookingLimits: undefined,
|
||||
durationLimits: undefined,
|
||||
recurringEvent: undefined,
|
||||
hidden: false,
|
||||
hashedLink: undefined,
|
||||
workflows: undefined,
|
||||
scheduleId: undefined,
|
||||
},
|
||||
where: {
|
||||
userId_parentId: {
|
||||
|
||||
@@ -9,6 +9,7 @@ import type Stripe from "stripe";
|
||||
import type { getMockRequestDataForBooking } from "test/utils/bookingScenario/getMockRequestDataForBooking";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import "vitest-fetch-mock";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { appStoreMetadata } from "@calcom/app-store/appStoreMetaData";
|
||||
import { handleStripePaymentSuccess } from "@calcom/features/ee/payments/api/webhook";
|
||||
@@ -20,6 +21,7 @@ import { ProfileRepository } from "@calcom/lib/server/repository/profile";
|
||||
import type { WorkflowActions, WorkflowTemplates, WorkflowTriggerEvents } from "@calcom/prisma/client";
|
||||
import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { userMetadataType } from "@calcom/prisma/zod-utils";
|
||||
import type { AppMeta } from "@calcom/types/App";
|
||||
import type { NewCalendarEventType } from "@calcom/types/Calendar";
|
||||
@@ -620,11 +622,19 @@ export async function createBookingScenario(data: ScenarioData) {
|
||||
};
|
||||
}
|
||||
|
||||
export async function createOrganization(orgData: { name: string; slug: string }) {
|
||||
export async function createOrganization(orgData: {
|
||||
name: string;
|
||||
slug: string;
|
||||
metadata?: z.infer<typeof teamMetadataSchema>;
|
||||
}) {
|
||||
const org = await prismock.team.create({
|
||||
data: {
|
||||
name: orgData.name,
|
||||
slug: orgData.slug,
|
||||
metadata: {
|
||||
...(orgData.metadata || {}),
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
return org;
|
||||
|
||||
@@ -37,6 +37,7 @@ export const getSampleUserInSession = function () {
|
||||
disableImpersonation: false,
|
||||
organizationId: null,
|
||||
theme: "",
|
||||
appTheme: "",
|
||||
createdDate: new Date(),
|
||||
trialEndsAt: new Date(),
|
||||
completedOnboarding: false,
|
||||
|
||||
@@ -4,12 +4,15 @@
|
||||
ARG NODE_VERSION=20.7.0
|
||||
FROM node:${NODE_VERSION}-slim as base
|
||||
|
||||
ARG DATABASE_URL
|
||||
|
||||
# Next.js/Prisma app lives here
|
||||
WORKDIR /app
|
||||
|
||||
# Set production environment
|
||||
ENV NODE_ENV="production"
|
||||
|
||||
ENV NODE_ENV="production" \
|
||||
DATABASE_URL=$DATABASE_URL \
|
||||
DATABASE_DIRECT_URL=$DATABASE_URL
|
||||
|
||||
# Throw-away build stage to reduce size of final image
|
||||
FROM base as build
|
||||
@@ -21,6 +24,7 @@ COPY ./apps/api ./apps/api
|
||||
COPY ./packages ./packages
|
||||
# TODO: follow up pr to remove dependencies on web
|
||||
COPY ./apps/web ./apps/web
|
||||
COPY ./tests ./tests
|
||||
|
||||
# Install node modules and dependencies, prune unneeded deps, then build
|
||||
RUN set -eux; \
|
||||
@@ -66,4 +70,4 @@ COPY --from=build /app/apps/web/package.json ./apps/web/package.json
|
||||
EXPOSE 80
|
||||
|
||||
# Start cmd, called when docker image is mounted
|
||||
CMD [ "yarn", "workspace", "@calcom/api", "docker-start-api"]
|
||||
CMD [ "yarn", "workspace", "@calcom/api", "docker-start-api"]
|
||||
|
||||
@@ -1,13 +1,25 @@
|
||||
import { APP_CREDENTIAL_SHARING_ENABLED } from "@calcom/lib/constants";
|
||||
import {
|
||||
APP_CREDENTIAL_SHARING_ENABLED,
|
||||
CREDENTIAL_SYNC_SECRET,
|
||||
CREDENTIAL_SYNC_SECRET_HEADER_NAME,
|
||||
} from "@calcom/lib/constants";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const refreshOAuthTokens = async (refreshFunction: () => any, appSlug: string, userId: number | null) => {
|
||||
// Check that app syncing is enabled and that the credential belongs to a user
|
||||
if (APP_CREDENTIAL_SHARING_ENABLED && process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT && userId) {
|
||||
if (
|
||||
APP_CREDENTIAL_SHARING_ENABLED &&
|
||||
process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT &&
|
||||
CREDENTIAL_SYNC_SECRET &&
|
||||
userId
|
||||
) {
|
||||
// Customize the payload based on what your endpoint requires
|
||||
// The response should only contain the access token and expiry date
|
||||
const response = await fetch(process.env.CALCOM_CREDENTIAL_SYNC_ENDPOINT, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
[CREDENTIAL_SYNC_SECRET_HEADER_NAME]: CREDENTIAL_SYNC_SECRET,
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
calcomUserId: userId.toString(),
|
||||
appSlug,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
||||
import { appKeysSchema as gtm_zod_ts } from "./gtm/zod";
|
||||
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
|
||||
import { appKeysSchema as intercom_zod_ts } from "./intercom/zod";
|
||||
import { appKeysSchema as jelly_zod_ts } from "./jelly/zod";
|
||||
import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
|
||||
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
||||
import { appKeysSchema as make_zod_ts } from "./make/zod";
|
||||
@@ -53,6 +54,7 @@ export const appKeysSchemas = {
|
||||
gtm: gtm_zod_ts,
|
||||
hubspot: hubspot_zod_ts,
|
||||
intercom: intercom_zod_ts,
|
||||
jelly: jelly_zod_ts,
|
||||
jitsivideo: jitsivideo_zod_ts,
|
||||
larkcalendar: larkcalendar_zod_ts,
|
||||
make: make_zod_ts,
|
||||
|
||||
@@ -14,6 +14,7 @@ import closecom_config_json from "./closecom/config.json";
|
||||
import cron_config_json from "./cron/config.json";
|
||||
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
|
||||
import deel_config_json from "./deel/config.json";
|
||||
import demodesk_config_json from "./demodesk/config.json";
|
||||
import discord_config_json from "./discord/config.json";
|
||||
import eightxeight_config_json from "./eightxeight/config.json";
|
||||
import element_call_config_json from "./element-call/config.json";
|
||||
@@ -32,8 +33,10 @@ import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
|
||||
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
|
||||
import ics_feedcalendar_config_json from "./ics-feedcalendar/config.json";
|
||||
import intercom_config_json from "./intercom/config.json";
|
||||
import jelly_config_json from "./jelly/config.json";
|
||||
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
||||
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
|
||||
import linear_config_json from "./linear/config.json";
|
||||
import make_config_json from "./make/config.json";
|
||||
import matomo_config_json from "./matomo/config.json";
|
||||
import metapixel_config_json from "./metapixel/config.json";
|
||||
@@ -96,6 +99,7 @@ export const appStoreMetadata = {
|
||||
cron: cron_config_json,
|
||||
dailyvideo: dailyvideo__metadata_ts,
|
||||
deel: deel_config_json,
|
||||
demodesk: demodesk_config_json,
|
||||
discord: discord_config_json,
|
||||
eightxeight: eightxeight_config_json,
|
||||
"element-call": element_call_config_json,
|
||||
@@ -114,8 +118,10 @@ export const appStoreMetadata = {
|
||||
huddle01video: huddle01video__metadata_ts,
|
||||
"ics-feedcalendar": ics_feedcalendar_config_json,
|
||||
intercom: intercom_config_json,
|
||||
jelly: jelly_config_json,
|
||||
jitsivideo: jitsivideo__metadata_ts,
|
||||
larkcalendar: larkcalendar__metadata_ts,
|
||||
linear: linear_config_json,
|
||||
make: make_config_json,
|
||||
matomo: matomo_config_json,
|
||||
metapixel: metapixel_config_json,
|
||||
|
||||
@@ -13,6 +13,7 @@ import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
||||
import { appDataSchema as gtm_zod_ts } from "./gtm/zod";
|
||||
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
|
||||
import { appDataSchema as intercom_zod_ts } from "./intercom/zod";
|
||||
import { appDataSchema as jelly_zod_ts } from "./jelly/zod";
|
||||
import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
|
||||
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
||||
import { appDataSchema as make_zod_ts } from "./make/zod";
|
||||
@@ -53,6 +54,7 @@ export const appDataSchemas = {
|
||||
gtm: gtm_zod_ts,
|
||||
hubspot: hubspot_zod_ts,
|
||||
intercom: intercom_zod_ts,
|
||||
jelly: jelly_zod_ts,
|
||||
jitsivideo: jitsivideo_zod_ts,
|
||||
larkcalendar: larkcalendar_zod_ts,
|
||||
make: make_zod_ts,
|
||||
|
||||
@@ -14,6 +14,7 @@ export const apiHandlers = {
|
||||
closecom: import("./closecom/api"),
|
||||
cron: import("./cron/api"),
|
||||
deel: import("./deel/api"),
|
||||
demodesk: import("./demodesk/api"),
|
||||
discord: import("./discord/api"),
|
||||
eightxeight: import("./eightxeight/api"),
|
||||
"element-call": import("./element-call/api"),
|
||||
@@ -32,8 +33,10 @@ export const apiHandlers = {
|
||||
huddle01video: import("./huddle01video/api"),
|
||||
"ics-feedcalendar": import("./ics-feedcalendar/api"),
|
||||
intercom: import("./intercom/api"),
|
||||
jelly: import("./jelly/api"),
|
||||
jitsivideo: import("./jitsivideo/api"),
|
||||
larkcalendar: import("./larkcalendar/api"),
|
||||
linear: import("./linear/api"),
|
||||
make: import("./make/api"),
|
||||
matomo: import("./matomo/api"),
|
||||
metapixel: import("./metapixel/api"),
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import around_config_json from "./around/config.json";
|
||||
import campfire_config_json from "./campfire/config.json";
|
||||
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
|
||||
import demodesk_config_json from "./demodesk/config.json";
|
||||
import discord_config_json from "./discord/config.json";
|
||||
import eightxeight_config_json from "./eightxeight/config.json";
|
||||
import element_call_config_json from "./element-call/config.json";
|
||||
@@ -14,6 +15,7 @@ import ga4_config_json from "./ga4/config.json";
|
||||
import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
|
||||
import gtm_config_json from "./gtm/config.json";
|
||||
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
|
||||
import jelly_config_json from "./jelly/config.json";
|
||||
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
||||
import matomo_config_json from "./matomo/config.json";
|
||||
import metapixel_config_json from "./metapixel/config.json";
|
||||
@@ -41,6 +43,7 @@ export const appStoreMetadata = {
|
||||
around: around_config_json,
|
||||
campfire: campfire_config_json,
|
||||
dailyvideo: dailyvideo__metadata_ts,
|
||||
demodesk: demodesk_config_json,
|
||||
discord: discord_config_json,
|
||||
eightxeight: eightxeight_config_json,
|
||||
"element-call": element_call_config_json,
|
||||
@@ -50,6 +53,7 @@ export const appStoreMetadata = {
|
||||
googlevideo: googlevideo__metadata_ts,
|
||||
gtm: gtm_config_json,
|
||||
huddle01video: huddle01video__metadata_ts,
|
||||
jelly: jelly_config_json,
|
||||
jitsivideo: jitsivideo__metadata_ts,
|
||||
matomo: matomo_config_json,
|
||||
metapixel: metapixel_config_json,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user