Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b06e46342c | ||
|
|
4cf482f860 | ||
|
|
f2478ea7d3 | ||
|
|
0e110022fb | ||
|
|
ee80577cc3 | ||
|
|
8402dfa3a2 | ||
|
|
4585199b5e | ||
|
|
ae2446f42f | ||
|
|
2f6d109a70 | ||
|
|
7ff884ca7d | ||
|
|
9230b2d2ca | ||
|
|
1a645f3110 | ||
|
|
4eb11e76ea | ||
|
|
e3a5ce5f6e | ||
|
|
12e171823b | ||
|
|
3c723d6f5b | ||
|
|
1482d9a9af | ||
|
|
0efe25feb4 | ||
|
|
602192e026 | ||
|
|
aeb54e6401 | ||
|
|
b8c1d623f3 | ||
|
|
30e4d769e3 | ||
|
|
3babfa7cf3 | ||
|
|
e45ee8fab3 | ||
|
|
3c96e122b4 | ||
|
|
f21a915814 | ||
|
|
3467e2272f | ||
|
|
844592e15e | ||
|
|
bdacb18d5c | ||
|
|
1b5d50c45c | ||
|
|
1c39dbad3d | ||
|
|
e60454750e | ||
|
|
286f696302 | ||
|
|
8deaea3edd | ||
|
|
7d497a621f | ||
|
|
5c8e0483b1 | ||
|
|
3cefa94a94 | ||
|
|
30549d5c14 | ||
|
|
b87972399e | ||
|
|
0784df6a31 | ||
|
|
fc062878ed | ||
|
|
33c1d80a42 | ||
|
|
1064e1181d | ||
|
|
dfd683ffc4 | ||
|
|
0972307998 | ||
|
|
157f633ef2 | ||
|
|
cc1c3b210a | ||
|
|
fcfd47e7d2 | ||
|
|
284c4c9084 | ||
|
|
9fd3594f6c | ||
|
|
531371bf55 | ||
|
|
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 |
+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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
name: Manually Delete BuildJet Cache
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
cache_key:
|
||||
description: "BuildJet Cache Key to Delete"
|
||||
required: true
|
||||
type: string
|
||||
jobs:
|
||||
manually-delete-buildjet-cache:
|
||||
runs-on: buildjet-2vcpu-ubuntu-2204
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: buildjet/cache-delete@v1
|
||||
with:
|
||||
cache_key: ${{ inputs.cache_key }}
|
||||
+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;
|
||||
|
||||
+17
-7
@@ -2,11 +2,9 @@ import { dir } from "i18next";
|
||||
import { Inter } from "next/font/google";
|
||||
import localFont from "next/font/local";
|
||||
import { headers, cookies } from "next/headers";
|
||||
import Script from "next/script";
|
||||
import React from "react";
|
||||
|
||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
|
||||
import { prepareRootMetadata } from "@lib/metadata";
|
||||
|
||||
@@ -18,6 +16,7 @@ const calFont = localFont({
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "block",
|
||||
weight: "600",
|
||||
});
|
||||
|
||||
export const generateMetadata = () =>
|
||||
@@ -69,11 +68,13 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
style={embedColorScheme ? { colorScheme: embedColorScheme as string } : undefined}
|
||||
data-nextjs-router="app">
|
||||
<head nonce={nonce}>
|
||||
{!IS_PRODUCTION && process.env.VERCEL_ENV === "preview" && (
|
||||
// eslint-disable-next-line @next/next/no-sync-scripts
|
||||
<Script
|
||||
data-project-id="KjpMrKTnXquJVKfeqmjdTffVPf1a6Unw2LZ58iE4"
|
||||
src="https://snippet.meticulous.ai/v1/stagingMeticulousSnippet.js"
|
||||
{!!process.env.NEXT_PUBLIC_HEAD_SCRIPTS && (
|
||||
<script
|
||||
nonce={nonce}
|
||||
id="injected-head-scripts"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: process.env.NEXT_PUBLIC_HEAD_SCRIPTS,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<style>{`
|
||||
@@ -97,6 +98,15 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
}
|
||||
: {}
|
||||
}>
|
||||
{!!process.env.NEXT_PUBLIC_BODY_SCRIPTS && (
|
||||
<script
|
||||
nonce={nonce}
|
||||
id="injected-head-scripts"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: process.env.NEXT_PUBLIC_BODY_SCRIPTS,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -24,6 +24,7 @@ const calFont = localFont({
|
||||
variable: "--font-cal",
|
||||
preload: true,
|
||||
display: "swap",
|
||||
weight: "600",
|
||||
});
|
||||
|
||||
function PageWrapper(props: AppProps) {
|
||||
@@ -75,6 +76,26 @@ function PageWrapper(props: AppProps) {
|
||||
id="page-status"
|
||||
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||
/>
|
||||
{process.env.NEXT_PUBLIC_GTM_ID ? (
|
||||
<>
|
||||
<Script
|
||||
id="gtm-init-script"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(function (w, d, s, l, i) {
|
||||
w[l] = w[l] || []; w[l].push({ 'gtm.start': new Date().getTime(), event: 'gtm.js' });
|
||||
var f = d.getElementsByTagName(s)[0], j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : '';
|
||||
j.async = true; j.src = 'https://www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
|
||||
})(window, document, 'script', 'dataLayer', '${process.env.NEXT_PUBLIC_GTM_ID}');`,
|
||||
}}
|
||||
/>
|
||||
<noscript
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `<iframe src="https://www.googletagmanager.com/ns.html?id=${process.env.NEXT_PUBLIC_GTM_ID}" height="0" width="0" style="display:none;visibility:hidden"></iframe>`,
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : null}
|
||||
|
||||
<style jsx global>{`
|
||||
:root {
|
||||
--font-inter: ${interFont.style.fontFamily};
|
||||
|
||||
@@ -5,7 +5,7 @@ import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { getEventLocationTypeFromApp, type EventLocationType } from "@calcom/app-store/locations";
|
||||
import type { CredentialOwner } from "@calcom/app-store/types";
|
||||
import { AppSetDefaultLinkDialog } from "@calcom/features/apps/components/AppSetDefaultLinkDialog";
|
||||
import { BulkEditDefaultConferencingModal } from "@calcom/features/eventtypes/components/BulkEditDefaultConferencingModal";
|
||||
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { AppCategories } from "@calcom/prisma/enums";
|
||||
import { trpc, type RouterOutputs } from "@calcom/trpc";
|
||||
@@ -154,6 +154,12 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
});
|
||||
|
||||
const { t } = useLocale();
|
||||
const updateLocationsMutation = trpc.viewer.eventTypes.bulkUpdateToDefaultLocation.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.getUsersDefaultConferencingApp.invalidate();
|
||||
setBulkUpdateModal(false);
|
||||
},
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<List className={listClassName}>
|
||||
@@ -173,7 +179,12 @@ export const AppList = ({ data, handleDisconnect, variant, listClassName }: AppL
|
||||
)}
|
||||
|
||||
{bulkUpdateModal && (
|
||||
<BulkEditDefaultConferencingModal open={bulkUpdateModal} setOpen={setBulkUpdateModal} />
|
||||
<BulkEditDefaultForEventsModal
|
||||
bulkUpdateFunction={updateLocationsMutation.mutate}
|
||||
open={bulkUpdateModal}
|
||||
setOpen={setBulkUpdateModal}
|
||||
isPending={updateLocationsMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -162,7 +162,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel"),
|
||||
label: isTabRecurring && isRecurring ? t("cancel_all_remaining") : t("cancel_event"),
|
||||
/* When cancelling we need to let the UI and the API know if the intention is to
|
||||
cancel all remaining bookings or just that booking instance. */
|
||||
href: `/booking/${booking.uid}?cancel=true${
|
||||
@@ -297,6 +297,8 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
];
|
||||
|
||||
const showPendingPayment = paymentAppData.enabled && booking.payment.length && !booking.paid;
|
||||
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
@@ -463,7 +465,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{booking.eventType.team.name}
|
||||
</Badge>
|
||||
)}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
{showPendingPayment && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2 sm:hidden" variant="orange">
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
@@ -490,8 +492,8 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{title}
|
||||
<span> </span>
|
||||
|
||||
{paymentAppData.enabled && !booking.paid && booking.payment.length && (
|
||||
<Badge className="me-2 ms-2 hidden sm:inline-flex" variant="orange">
|
||||
{showPendingPayment && (
|
||||
<Badge className="hidden sm:inline-flex" variant="orange">
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InfoIcon } from "lucide-react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { z } from "zod";
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
allowDisablingHostConfirmationEmails,
|
||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||
import type { EditableSchema } from "@calcom/features/form-builder/schema";
|
||||
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||
@@ -27,8 +28,10 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Badge,
|
||||
CheckboxField,
|
||||
Label,
|
||||
SelectField,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
Switch,
|
||||
@@ -53,7 +56,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
formMethods.getValues("useEventTypeDestinationCalendarEmail")
|
||||
);
|
||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||
|
||||
const bookingFields: Prisma.JsonObject = {};
|
||||
|
||||
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
|
||||
@@ -84,6 +86,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
|
||||
useEffect(() => {
|
||||
!hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [formMethods.getValues("users"), hashedUrl, team?.id]);
|
||||
|
||||
const toggleGuests = (enabled: boolean) => {
|
||||
@@ -106,11 +109,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
);
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { isChildrenManagedEventType, isManagedEventType, shouldLockDisableProps } = useLockedFieldsManager({
|
||||
eventType,
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
const eventNamePlaceholder = getEventName({
|
||||
...eventNameObject,
|
||||
eventName: formMethods.watch("eventName"),
|
||||
@@ -118,10 +121,24 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
|
||||
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
|
||||
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||
const requiresBookerEmailVerificationProps = shouldLockDisableProps("requiresBookerEmailVerification");
|
||||
const hideCalendarNotesLocked = shouldLockDisableProps("hideCalendarNotes");
|
||||
const lockTimeZoneToggleOnBookingPageLocked = shouldLockDisableProps("lockTimeZoneToggleOnBookingPage");
|
||||
|
||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
const displayDestinationCalendarSelector =
|
||||
!!connectedCalendarsQuery.data?.connectedCalendars.length && !team;
|
||||
!!connectedCalendarsQuery.data?.connectedCalendars.length && (!team || isChildrenManagedEventType);
|
||||
|
||||
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">
|
||||
@@ -169,6 +186,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
{...(shouldLockDisableProps("eventName").disabled ? { disabled: true } : {})}
|
||||
aria-label="edit custom name"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
onClick={() => setShowEventNameTip((old) => !old)}>
|
||||
@@ -198,10 +216,30 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
/>
|
||||
</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">{t("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} />
|
||||
|
||||
<div className="border-subtle space-y-6 rounded-lg border p-6">
|
||||
<FormBuilder
|
||||
title={t("booking_questions_title")}
|
||||
@@ -216,7 +254,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<RequiresConfirmationController
|
||||
eventType={eventType}
|
||||
seatsEnabled={seatsEnabled}
|
||||
@@ -224,7 +261,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
requiresConfirmation={requiresConfirmation}
|
||||
onRequiresConfirmation={setRequiresConfirmation}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="requiresBookerEmailVerification"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
@@ -234,14 +270,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("requires_booker_email_verification")}
|
||||
data-testid="requires-booker-email-verification"
|
||||
{...shouldLockDisableProps("requiresBookerEmailVerification")}
|
||||
{...requiresBookerEmailVerificationProps}
|
||||
description={t("description_requires_booker_email_verification")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="hideCalendarNotes"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
@@ -249,16 +284,15 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("disable_notes")}
|
||||
data-testid="disable-notes"
|
||||
{...shouldLockDisableProps("hideCalendarNotes")}
|
||||
title={t("disable_notes")}
|
||||
{...hideCalendarNotesLocked}
|
||||
description={t("disable_notes_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Controller
|
||||
name="successRedirectUrl"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
@@ -305,7 +339,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
@@ -325,52 +358,53 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<Info className="ml-1.5 h-4 w-4 cursor-pointer" />
|
||||
</a>
|
||||
}
|
||||
{...shouldLockDisableProps("hashedLinkCheck")}
|
||||
{...shouldLockDisableProps("hashedLink")}
|
||||
description={t("private_link_description", { appName: APP_NAME })}
|
||||
checked={hashedLinkVisible}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hashedLink", e ? hashedUrl : undefined, { shouldDirty: true });
|
||||
setHashedLinkVisible(e);
|
||||
}}>
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
{!IS_VISUAL_REGRESSION_TESTING && (
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
labelSrOnly
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip
|
||||
content={
|
||||
formMethods.getValues("hashedLink") ? t("copy_to_clipboard") : t("enabled_after_update")
|
||||
}>
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (formMethods.getValues("hashedLink")) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!isManagedEventType && (
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
{!IS_VISUAL_REGRESSION_TESTING && (
|
||||
<TextField
|
||||
disabled
|
||||
name="hashedLink"
|
||||
label={t("private_link_label")}
|
||||
data-testid="generated-hash-url"
|
||||
labelSrOnly
|
||||
type="text"
|
||||
hint={t("private_link_hint")}
|
||||
defaultValue={placeholderHashedLink}
|
||||
addOnSuffix={
|
||||
<Tooltip
|
||||
content={
|
||||
formMethods.getValues("hashedLink") ? t("copy_to_clipboard") : t("enabled_after_update")
|
||||
}>
|
||||
<Button
|
||||
color="minimal"
|
||||
size="sm"
|
||||
type="button"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit !py-0 px-0 hover:bg-transparent"
|
||||
aria-label="copy link"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (formMethods.getValues("hashedLink")) {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</SettingsToggle>
|
||||
|
||||
<Controller
|
||||
name="seatsPerTimeSlotEnabled"
|
||||
render={({ field: { value, onChange } }) => (
|
||||
@@ -470,7 +504,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={t("lock_timezone_toggle_on_booking_page")}
|
||||
{...shouldLockDisableProps("lockTimeZoneToggleOnBookingPage")}
|
||||
{...lockTimeZoneToggleOnBookingPageLocked}
|
||||
description={t("description_lock_timezone_toggle_on_booking_page")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
|
||||
@@ -8,6 +8,7 @@ import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCard
|
||||
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, EmptyScreen, Alert } from "@calcom/ui";
|
||||
@@ -74,11 +75,13 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
};
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
const appsDisableProps = shouldLockDisableProps("apps", { simple: true });
|
||||
const lockedText = appsDisableProps.isLocked ? "locked" : "unlocked";
|
||||
|
||||
const appsWithTeamCredentials = eventTypeApps?.items.filter((app) => app.teams.length) || [];
|
||||
const cardsForAppsWithTeams = appsWithTeamCredentials.map((app) => {
|
||||
@@ -97,7 +100,6 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
{...shouldLockDisableProps("apps")}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -121,7 +123,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
}}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
{...shouldLockDisableProps("apps")}
|
||||
disabled={shouldLockDisableProps("apps").disabled}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -133,12 +135,28 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
<>
|
||||
<div>
|
||||
<div className="before:border-0">
|
||||
{isManagedEventType && (
|
||||
{(isManagedEventType || isChildrenManagedEventType) && (
|
||||
<Alert
|
||||
severity="neutral"
|
||||
severity={appsDisableProps.isLocked ? "neutral" : "green"}
|
||||
className="mb-2"
|
||||
title={t("locked_for_members")}
|
||||
message={t("locked_apps_description")}
|
||||
title={
|
||||
<Trans i18nKey={`${lockedText}_${isManagedEventType ? "for_members" : "by_team_admins"}`}>
|
||||
{lockedText[0].toUpperCase()}
|
||||
{lockedText.slice(1)} {isManagedEventType ? "for members" : "by team admins"}
|
||||
</Trans>
|
||||
}
|
||||
actions={<div className="flex h-full items-center">{appsDisableProps.LockedIcon}</div>}
|
||||
message={
|
||||
<Trans
|
||||
i18nKey={`apps_${lockedText}_${
|
||||
isManagedEventType ? "for_members" : "by_team_admins"
|
||||
}_description`}>
|
||||
{isManagedEventType ? "Members" : "You"}{" "}
|
||||
{appsDisableProps.isLocked
|
||||
? "will be able to see the active apps but will not be able to edit any app settings"
|
||||
: "will be able to see the active apps and will be able to edit any app settings"}
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{!isPending && !installedApps?.length ? (
|
||||
@@ -147,9 +165,9 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
headline={t("empty_installed_apps_headline")}
|
||||
description={t("empty_installed_apps_description")}
|
||||
buttonRaw={
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
appsDisableProps.disabled ? (
|
||||
<Button StartIcon={Lock} color="secondary" disabled>
|
||||
{t("locked_by_admin")}
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button target="_blank" color="secondary" href="/apps">
|
||||
@@ -174,13 +192,12 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
eventTypeFormMetadata={eventTypeFormMetadata}
|
||||
{...shouldLockDisableProps("apps")}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
{!shouldLockDisableProps("apps").disabled && (
|
||||
{!appsDisableProps.disabled && (
|
||||
<div className="bg-muted mt-6 rounded-md p-8">
|
||||
{!isPending && notInstalledApps?.length ? (
|
||||
<>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState, memo, useEffect } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { OptionProps, SingleValueProps } from "react-select";
|
||||
@@ -6,6 +6,7 @@ import { components } from "react-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { AvailabilityOption, FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
@@ -17,13 +18,6 @@ import { ExternalLink, Globe } from "@calcom/ui/components/icon";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
export type AvailabilityOption = {
|
||||
label: string;
|
||||
value: number;
|
||||
isDefault: boolean;
|
||||
isManaged?: boolean;
|
||||
};
|
||||
|
||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||
const { label, isDefault, isManaged = false } = props.data;
|
||||
const { t } = useLocale();
|
||||
@@ -160,12 +154,10 @@ EventTypeScheduleDetails.displayName = "EventTypeScheduleDetails";
|
||||
|
||||
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const { t } = useLocale();
|
||||
const { shouldLockIndicator, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { watch, setValue, getValues } = useFormContext<FormValues>();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } =
|
||||
useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
const { watch, setValue, getValues } = formMethods;
|
||||
const watchSchedule = watch("schedule");
|
||||
const [options, setOptions] = useState<AvailabilityOption[]>([]);
|
||||
|
||||
@@ -246,7 +238,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
<div className="border-subtle rounded-t-md border p-6">
|
||||
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
|
||||
{t("availability")}
|
||||
{shouldLockIndicator("availability")}
|
||||
{(isManagedEventType || isChildrenManagedEventType) && shouldLockIndicator("availability")}
|
||||
</label>
|
||||
{isPending && <SelectSkeletonLoader />}
|
||||
{!isPending && (
|
||||
@@ -257,6 +249,7 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
<Select
|
||||
placeholder={t("select")}
|
||||
options={options}
|
||||
isDisabled={shouldLockDisableProps("availability").disabled}
|
||||
isSearchable={false}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import type { Key } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type { SingleValue } from "react-select";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||
@@ -108,7 +110,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
export const EventLimitsTab = () => {
|
||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||
const { t, i18n } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
@@ -129,6 +131,18 @@ export const EventLimitsTab = () => {
|
||||
|
||||
const watchPeriodType = formMethods.watch("periodType");
|
||||
|
||||
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager({
|
||||
eventType,
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
|
||||
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
||||
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
||||
const onlyFirstAvailableSlotLocked = shouldLockDisableProps("onlyShowFirstAvailableSlot");
|
||||
const periodTypeLocked = shouldLockDisableProps("periodType");
|
||||
const offsetStartLockedProps = shouldLockDisableProps("offsetStart");
|
||||
|
||||
const optionsPeriod = [
|
||||
{ value: 0, label: t("business_days") },
|
||||
{ value: 1, label: t("calendar_days") },
|
||||
@@ -147,7 +161,10 @@ export const EventLimitsTab = () => {
|
||||
<div className="border-subtle space-y-6 rounded-lg border p-6">
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="beforeEventBuffer">{t("before_event")}</Label>
|
||||
<Label htmlFor="beforeBufferTime">
|
||||
{t("before_event")}
|
||||
{shouldLockIndicator("beforeBufferTime")}
|
||||
</Label>
|
||||
<Controller
|
||||
name="beforeEventBuffer"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
@@ -167,6 +184,7 @@ export const EventLimitsTab = () => {
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
isDisabled={shouldLockDisableProps("beforeBufferTime").disabled}
|
||||
defaultValue={
|
||||
beforeBufferOptions.find((option) => option.value === value) || beforeBufferOptions[0]
|
||||
}
|
||||
@@ -177,7 +195,10 @@ export const EventLimitsTab = () => {
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="afterEventBuffer">{t("after_event")}</Label>
|
||||
<Label htmlFor="afterBufferTime">
|
||||
{t("after_event")}
|
||||
{shouldLockIndicator("afterBufferTime")}
|
||||
</Label>
|
||||
<Controller
|
||||
name="afterEventBuffer"
|
||||
render={({ field: { onChange, value } }) => {
|
||||
@@ -197,6 +218,7 @@ export const EventLimitsTab = () => {
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
isDisabled={shouldLockDisableProps("afterBufferTime").disabled}
|
||||
defaultValue={
|
||||
afterBufferOptions.find((option) => option.value === value) || afterBufferOptions[0]
|
||||
}
|
||||
@@ -209,11 +231,20 @@ export const EventLimitsTab = () => {
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-x-4 lg:space-y-0">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")}</Label>
|
||||
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
|
||||
<Label htmlFor="minimumBookingNotice">
|
||||
{t("minimum_booking_notice")}
|
||||
{shouldLockIndicator("minimumBookingNotice")}
|
||||
</Label>
|
||||
<MinimumBookingNoticeInput
|
||||
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
|
||||
{...formMethods.register("minimumBookingNotice")}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
|
||||
<Label htmlFor="slotInterval">
|
||||
{t("slot_interval")}
|
||||
{shouldLockIndicator("slotInterval")}
|
||||
</Label>
|
||||
<Controller
|
||||
name="slotInterval"
|
||||
render={() => {
|
||||
@@ -230,6 +261,7 @@ export const EventLimitsTab = () => {
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
isDisabled={shouldLockDisableProps("slotInterval").disabled}
|
||||
onChange={(val) => {
|
||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null, {
|
||||
shouldDirty: true,
|
||||
@@ -257,6 +289,7 @@ export const EventLimitsTab = () => {
|
||||
toggleSwitchAtTheEnd={true}
|
||||
labelClassName="text-sm"
|
||||
title={t("limit_booking_frequency")}
|
||||
{...bookingLimitsLocked}
|
||||
description={t("limit_booking_frequency_description")}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
@@ -278,7 +311,12 @@ export const EventLimitsTab = () => {
|
||||
)}
|
||||
childrenClassName="lg:ml-0">
|
||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
||||
<IntervalLimitsManager
|
||||
disabled={bookingLimitsLocked.disabled}
|
||||
propertyName="bookingLimits"
|
||||
defaultLimit={1}
|
||||
step={1}
|
||||
/>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
);
|
||||
@@ -295,6 +333,7 @@ export const EventLimitsTab = () => {
|
||||
title={t("limit_booking_only_first_slot")}
|
||||
description={t("limit_booking_only_first_slot_description")}
|
||||
checked={isChecked}
|
||||
{...onlyFirstAvailableSlotLocked}
|
||||
onCheckedChange={(active) => {
|
||||
onChange(active ?? false);
|
||||
}}
|
||||
@@ -321,6 +360,7 @@ export const EventLimitsTab = () => {
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("limit_total_booking_duration")}
|
||||
description={t("limit_total_booking_duration_description")}
|
||||
{...durationLimitsLocked}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
@@ -335,6 +375,7 @@ export const EventLimitsTab = () => {
|
||||
<IntervalLimitsManager
|
||||
propertyName="durationLimits"
|
||||
defaultLimit={60}
|
||||
disabled={durationLimitsLocked.disabled}
|
||||
step={15}
|
||||
textFieldSuffix={t("minutes")}
|
||||
/>
|
||||
@@ -359,6 +400,7 @@ export const EventLimitsTab = () => {
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("limit_future_bookings")}
|
||||
description={t("limit_future_bookings_description")}
|
||||
{...periodTypeLocked}
|
||||
checked={isChecked}
|
||||
onCheckedChange={(bool) => {
|
||||
if (bool && !formMethods.getValues("periodDays")) {
|
||||
@@ -372,7 +414,9 @@ export const EventLimitsTab = () => {
|
||||
onValueChange={(val) =>
|
||||
formMethods.setValue("periodType", val as PeriodType, { shouldDirty: true })
|
||||
}>
|
||||
{PERIOD_TYPES.map((period) => {
|
||||
{PERIOD_TYPES.filter((opt) =>
|
||||
periodTypeLocked.disabled ? watchPeriodType === opt.type : true
|
||||
).map((period) => {
|
||||
if (period.type === "UNLIMITED") return null;
|
||||
return (
|
||||
<div
|
||||
@@ -381,12 +425,14 @@ export const EventLimitsTab = () => {
|
||||
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
||||
)}
|
||||
key={period.type}>
|
||||
<RadioGroup.Item
|
||||
id={period.type}
|
||||
value={period.type}
|
||||
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
|
||||
</RadioGroup.Item>
|
||||
{!periodTypeLocked.disabled && (
|
||||
<RadioGroup.Item
|
||||
id={period.type}
|
||||
value={period.type}
|
||||
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
|
||||
</RadioGroup.Item>
|
||||
)}
|
||||
|
||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||
{period.type === "ROLLING" && (
|
||||
@@ -396,18 +442,20 @@ export const EventLimitsTab = () => {
|
||||
type="number"
|
||||
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||
placeholder="30"
|
||||
disabled={periodTypeLocked.disabled}
|
||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||
/>
|
||||
<Select
|
||||
options={optionsPeriod}
|
||||
isSearchable={false}
|
||||
onChange={(opt) =>
|
||||
isDisabled={periodTypeLocked.disabled}
|
||||
onChange={(opt) => {
|
||||
formMethods.setValue(
|
||||
"periodCountCalendarDays",
|
||||
opt?.value === 1 ? true : false,
|
||||
{ shouldDirty: true }
|
||||
)
|
||||
}
|
||||
);
|
||||
}}
|
||||
name="periodCoundCalendarDays"
|
||||
value={optionsPeriod.find((opt) => {
|
||||
opt.value ===
|
||||
@@ -429,6 +477,7 @@ export const EventLimitsTab = () => {
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
disabled={periodTypeLocked.disabled}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
onChange({
|
||||
startDate,
|
||||
@@ -460,6 +509,7 @@ export const EventLimitsTab = () => {
|
||||
childrenClassName="lg:ml-0"
|
||||
title={t("offset_toggle")}
|
||||
description={t("offset_toggle_description")}
|
||||
{...offsetStartLockedProps}
|
||||
checked={offsetToggle}
|
||||
onCheckedChange={(active) => {
|
||||
setOffsetToggle(active);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { ErrorMessage } from "@hookform/error-message";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext, useFieldArray } from "react-hook-form";
|
||||
import type { MultiValue } from "react-select";
|
||||
@@ -11,6 +11,7 @@ import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
@@ -142,12 +143,7 @@ export const EventSetupTab = (
|
||||
);
|
||||
|
||||
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
|
||||
useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
|
||||
useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
const Locations = () => {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
@@ -183,7 +179,6 @@ export const EventSetupTab = (
|
||||
index: number;
|
||||
}) => {
|
||||
const { eventLocationType, index, ...remainingProps } = props;
|
||||
|
||||
if (eventLocationType?.organizerInputType === "text") {
|
||||
const { defaultValue, ...rest } = remainingProps;
|
||||
|
||||
@@ -200,6 +195,7 @@ export const EventSetupTab = (
|
||||
required
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
{...(shouldLockDisableProps("locations").disabled ? { disabled: true } : {})}
|
||||
className="my-0"
|
||||
{...rest}
|
||||
/>
|
||||
@@ -218,6 +214,7 @@ export const EventSetupTab = (
|
||||
return (
|
||||
<PhoneInput
|
||||
required
|
||||
disabled={shouldLockDisableProps("locations").disabled}
|
||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||
name={`locations[${index}].${eventLocationType.defaultValueVariable}`}
|
||||
value={value}
|
||||
@@ -233,7 +230,10 @@ export const EventSetupTab = (
|
||||
};
|
||||
|
||||
const [showEmptyLocationSelect, setShowEmptyLocationSelect] = useState(false);
|
||||
const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(null);
|
||||
const defaultInitialLocation = defaultValue || null;
|
||||
const [selectedNewOption, setSelectedNewOption] = useState<SingleValueLocationOption | null>(
|
||||
defaultInitialLocation
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
@@ -288,16 +288,18 @@ export const EventSetupTab = (
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
data-testid={`delete-locations.${index}.type`}
|
||||
className="min-h-9 block h-9 px-2"
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
aria-label={t("remove")}>
|
||||
<div className="h-4 w-4">
|
||||
<X className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
|
||||
</div>
|
||||
</button>
|
||||
{!(shouldLockDisableProps("locations").disabled && isChildrenManagedEventType) && (
|
||||
<button
|
||||
data-testid={`delete-locations.${index}.type`}
|
||||
className="min-h-9 block h-9 px-2"
|
||||
type="button"
|
||||
onClick={() => remove(index)}
|
||||
aria-label={t("remove")}>
|
||||
<div className="h-4 w-4">
|
||||
<X className="border-l-1 hover:text-emphasis text-subtle h-4 w-4" />
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{eventLocationType?.organizerInputType && (
|
||||
@@ -329,6 +331,7 @@ export const EventSetupTab = (
|
||||
<CheckboxField
|
||||
name={`locations[${index}].displayLocationPublicly`}
|
||||
data-testid="display-location"
|
||||
disabled={shouldLockDisableProps("locations").disabled}
|
||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
||||
description={t("display_location_label")}
|
||||
onChange={(e) => {
|
||||
@@ -417,7 +420,8 @@ export const EventSetupTab = (
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
|
||||
{validLocations.length > 0 && !shouldLockDisableProps("locations").disabled && (
|
||||
// && !isChildrenManagedEventType : Add this to hide add-location button only when location is disabled by Admin
|
||||
<li>
|
||||
<Button
|
||||
data-testid="add-location"
|
||||
@@ -444,7 +448,8 @@ export const EventSetupTab = (
|
||||
|
||||
const lengthLockedProps = shouldLockDisableProps("length");
|
||||
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||
|
||||
const urlLockedProps = shouldLockDisableProps("slug");
|
||||
const titleLockedProps = shouldLockDisableProps("title");
|
||||
const urlPrefix = orgBranding
|
||||
? orgBranding?.fullDomain.replace(/^(https?:|)\/\//, "")
|
||||
: `${WEBSITE_URL?.replace(/^(https?:|)\/\//, "")}`;
|
||||
@@ -456,20 +461,22 @@ export const EventSetupTab = (
|
||||
<TextField
|
||||
required
|
||||
label={t("title")}
|
||||
{...shouldLockDisableProps("title")}
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? titleLockedProps : {})}
|
||||
defaultValue={eventType.title}
|
||||
{...formMethods.register("title")}
|
||||
/>
|
||||
<div>
|
||||
<Label>
|
||||
<Label htmlFor="editor">
|
||||
{t("description")}
|
||||
{shouldLockIndicator("description")}
|
||||
{(isManagedEventType || isChildrenManagedEventType) && shouldLockIndicator("description")}
|
||||
</Label>
|
||||
<DescriptionEditor isEditable={!descriptionLockedProps.disabled} />
|
||||
</div>
|
||||
<TextField
|
||||
required
|
||||
label={t("URL")}
|
||||
{...shouldLockDisableProps("slug")}
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? urlLockedProps : {})}
|
||||
defaultValue={eventType.slug}
|
||||
addOnLeading={
|
||||
<>
|
||||
{urlPrefix}/
|
||||
@@ -498,6 +505,7 @@ export const EventSetupTab = (
|
||||
defaultValue={selectedMultipleDuration}
|
||||
name="metadata.multipleDuration"
|
||||
isSearchable={false}
|
||||
isDisabled={lengthLockedProps.disabled}
|
||||
className="h-auto !min-h-[36px] text-sm"
|
||||
options={multipleDurationOptions}
|
||||
value={selectedMultipleDuration}
|
||||
@@ -551,7 +559,8 @@ export const EventSetupTab = (
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
{...lengthLockedProps}
|
||||
data-testid="duration"
|
||||
{...(isManagedEventType || isChildrenManagedEventType ? lengthLockedProps : {})}
|
||||
label={t("duration")}
|
||||
defaultValue={formMethods.getValues("length") ?? 15}
|
||||
{...formMethods.register("length")}
|
||||
@@ -583,15 +592,20 @@ export const EventSetupTab = (
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-subtle rounded-lg border p-6">
|
||||
<div>
|
||||
<Skeleton as={Label} loadingClassName="w-16">
|
||||
<Skeleton as={Label} loadingClassName="w-16" htmlFor="locations">
|
||||
{t("location")}
|
||||
{/*improve shouldLockIndicator function to also accept eventType and then conditionally render
|
||||
based on Managed Event type or not.*/}
|
||||
{shouldLockIndicator("locations")}
|
||||
</Skeleton>
|
||||
|
||||
<Controller name="locations" render={() => <Locations />} />
|
||||
<Controller
|
||||
name="locations"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.locations || []}
|
||||
render={() => <Locations />}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps, FormValues, Host } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps, Host } from "pages/event-types/[type]";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import type { ComponentProps, Dispatch, SetStateAction } from "react";
|
||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||
@@ -9,6 +9,7 @@ import type { Options } from "react-select";
|
||||
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
|
||||
@@ -2,12 +2,13 @@ import { Webhook as TbWebhook } from "lucide-react";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useRouter } from "next/navigation";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useMemo, useState, Suspense } from "react";
|
||||
import type { UseFormReturn } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { EventTypeEmbedButton, EventTypeEmbedDialog } from "@calcom/features/embed/EventTypeEmbed";
|
||||
import type { FormValues, AvailabilityOption } from "@calcom/features/eventtypes/lib/types";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -52,8 +53,6 @@ import {
|
||||
Loader,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
eventType: EventTypeSetupProps["eventType"];
|
||||
@@ -205,6 +204,7 @@ function EventTypeSingleLayout({
|
||||
activeWebhooksNumber,
|
||||
}: Props) {
|
||||
const { t } = useLocale();
|
||||
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
|
||||
const hasPermsToDelete =
|
||||
@@ -213,11 +213,11 @@ function EventTypeSingleLayout({
|
||||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
|
||||
isUserOrganizationAdmin;
|
||||
|
||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||
formMethods.getValues(),
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
|
||||
const length = formMethods.watch("length");
|
||||
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
|
||||
|
||||
@@ -4,8 +4,10 @@ import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
@@ -20,6 +22,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
const { t } = useLocale();
|
||||
|
||||
const utils = trpc.useContext();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
|
||||
|
||||
@@ -96,11 +99,11 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
);
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
translate: t,
|
||||
formMethods,
|
||||
});
|
||||
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||
|
||||
return (
|
||||
@@ -163,7 +166,7 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
||||
buttonRaw={
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
<Button StartIcon={Lock} color="secondary" disabled>
|
||||
{t("locked_by_admin")}
|
||||
{t("locked_by_team_admin")}
|
||||
</Button>
|
||||
) : (
|
||||
<NewWebhookButton />
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { Webhook } from "@prisma/client";
|
||||
import { useSession } from "next-auth/react";
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
@@ -33,11 +34,7 @@ export default function InstantEventController({
|
||||
const [instantEventState, setInstantEventState] = useState<boolean>(eventType?.isInstantEvent ?? false);
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
|
||||
const instantLocked = shouldLockDisableProps("isInstantEvent");
|
||||
|
||||
@@ -104,6 +101,8 @@ export default function InstantEventController({
|
||||
const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({
|
||||
eventTypeId: eventType.id,
|
||||
eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
|
||||
@@ -179,11 +178,11 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
|
||||
);
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager({
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
formMethods,
|
||||
translate: t,
|
||||
});
|
||||
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||
@@ -31,11 +32,7 @@ export default function RecurringEventController({
|
||||
value: value.toString(),
|
||||
}));
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
|
||||
const recurringLocked = shouldLockDisableProps("recurringEvent");
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { UnitTypeLongPlural } from "dayjs";
|
||||
import { Trans } from "next-i18next";
|
||||
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||
import type { EventTypeSetup } from "pages/event-types/[type]";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import type z from "zod";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -42,11 +43,7 @@ export default function RequiresConfirmationController({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [requiresConfirmation]);
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager({ eventType, translate: t, formMethods });
|
||||
const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
|
||||
|
||||
const options = [
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -78,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,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -134,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
|
||||
@@ -152,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: {
|
||||
@@ -167,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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -96,6 +96,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
isBrandingHidden: team?.hideBranding,
|
||||
isInstantMeeting: eventData.isInstantEvent && queryIsInstantMeeting ? true : false,
|
||||
themeBasis: null,
|
||||
orgBannerUrl: eventData?.team?.parent?.bannerUrl ?? "",
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
// Taking care of sub-teams and orgs
|
||||
if (
|
||||
(!isValidOrgDomain && team?.parent) ||
|
||||
(!isValidOrgDomain && !!metadata?.isOrganization) ||
|
||||
(!isValidOrgDomain && !!team?.isOrganization) ||
|
||||
!organizationsEnabled
|
||||
) {
|
||||
return { notFound: true } as const;
|
||||
@@ -90,6 +90,17 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
}),
|
||||
},
|
||||
include: {
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
name: true,
|
||||
isPrivate: true,
|
||||
isOrganization: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!unpublishedTeam) return { notFound: true } as const;
|
||||
@@ -103,19 +114,23 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
} as const;
|
||||
}
|
||||
|
||||
const isTeamOrParentOrgPrivate = team.isPrivate || (team.parent?.isOrganization && team.parent?.isPrivate);
|
||||
|
||||
team.eventTypes =
|
||||
team.eventTypes?.map((type) => ({
|
||||
...type,
|
||||
users: type.users.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
})),
|
||||
users: !isTeamOrParentOrgPrivate
|
||||
? type.users.map((user) => ({
|
||||
...user,
|
||||
avatar: `/${user.username}/avatar.png`,
|
||||
}))
|
||||
: [],
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
|
||||
})) ?? null;
|
||||
|
||||
const safeBio = markdownToSafeHTML(team.bio) || "";
|
||||
|
||||
const members = !team.isPrivate
|
||||
const members = !isTeamOrParentOrgPrivate
|
||||
? team.members.map((member) => {
|
||||
return {
|
||||
name: member.name,
|
||||
@@ -139,7 +154,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
|
||||
return {
|
||||
props: {
|
||||
team: { ...serializableTeam, safeBio, members, metadata },
|
||||
team: {
|
||||
...serializableTeam,
|
||||
safeBio,
|
||||
members,
|
||||
metadata,
|
||||
children: isTeamOrParentOrgPrivate ? [] : team.children,
|
||||
},
|
||||
themeBasis: serializableTeam.slug,
|
||||
trpcState: ssr.dehydrate(),
|
||||
markdownStrippedBio,
|
||||
|
||||
@@ -24,7 +24,11 @@ import {
|
||||
useIsEmbed,
|
||||
} from "@calcom/embed-core/embed-iframe";
|
||||
import { Price } from "@calcom/features/bookings/components/event-meta/Price";
|
||||
import { SMS_REMINDER_NUMBER_FIELD, SystemField } from "@calcom/features/bookings/lib/SystemField";
|
||||
import {
|
||||
SMS_REMINDER_NUMBER_FIELD,
|
||||
SystemField,
|
||||
TITLE_FIELD,
|
||||
} from "@calcom/features/bookings/lib/SystemField";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import {
|
||||
formatToLocalizedDate,
|
||||
@@ -91,6 +95,7 @@ export default function Success(props: PageProps) {
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { eventType, bookingInfo, requiresLoginToUpdate } = props;
|
||||
|
||||
const {
|
||||
allRemainingBookings,
|
||||
isSuccessBookingPage,
|
||||
@@ -345,7 +350,7 @@ export default function Success(props: PageProps) {
|
||||
!giphyImage && !isCancelled && needsConfirmation
|
||||
? "bg-subtle h-12 w-12 rounded-full"
|
||||
: "",
|
||||
isCancelled ? "bg-error h-12 w-12 rounded-full" : ""
|
||||
isCancelled ? "bg-error h-12 w-12 rounded-full " : ""
|
||||
)}>
|
||||
{giphyImage && !needsConfirmation && !isCancelled && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
@@ -518,7 +523,13 @@ export default function Success(props: PageProps) {
|
||||
if (!field) return null;
|
||||
const isSystemField = SystemField.safeParse(field.name);
|
||||
// SMS_REMINDER_NUMBER_FIELD is a system field but doesn't have a dedicated place in the UI. So, it would be shown through the following responses list
|
||||
if (isSystemField.success && field.name !== SMS_REMINDER_NUMBER_FIELD) return null;
|
||||
// TITLE is also an identifier for booking question "What is this meeting about?"
|
||||
if (
|
||||
isSystemField.success &&
|
||||
field.name !== SMS_REMINDER_NUMBER_FIELD &&
|
||||
field.name !== TITLE_FIELD
|
||||
)
|
||||
return null;
|
||||
|
||||
const label = field.label || t(field.defaultLabel || "");
|
||||
|
||||
|
||||
@@ -803,11 +803,10 @@ const CreateFirstEventTypeView = ({ slug }: { slug: string }) => {
|
||||
);
|
||||
};
|
||||
|
||||
const CTA = ({ data }: { data: GetByViewerResponse }) => {
|
||||
const CTA = ({ data, isOrganization }: { data: GetByViewerResponse; isOrganization: boolean }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
if (!data) return null;
|
||||
|
||||
const profileOptions = data.profiles
|
||||
.filter((profile) => !profile.readOnly)
|
||||
.map((profile) => {
|
||||
@@ -825,7 +824,9 @@ const CTA = ({ data }: { data: GetByViewerResponse }) => {
|
||||
data-testid="new-event-type"
|
||||
subtitle={t("create_event_on").toUpperCase()}
|
||||
options={profileOptions}
|
||||
createDialog={() => <CreateEventTypeDialog profileOptions={profileOptions} />}
|
||||
createDialog={() => (
|
||||
<CreateEventTypeDialog profileOptions={profileOptions} isOrganization={isOrganization} />
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -983,7 +984,7 @@ const EventTypesPage: React.FC & {
|
||||
hideHeadingOnMobile
|
||||
subtitle={t("event_types_page_subtitle")}
|
||||
beforeCTAactions={<Actions />}
|
||||
CTA={<CTA data={data} />}>
|
||||
CTA={<CTA data={data} isOrganization={!!user?.organizationId} />}>
|
||||
<HeadSeo
|
||||
title="Event Types"
|
||||
description="Create events to share for people to book on your calendar."
|
||||
|
||||
@@ -12,8 +12,8 @@ import { z } from "zod";
|
||||
import checkForMultiplePaymentApps from "@calcom/app-store/_utils/payments/checkForMultiplePaymentApps";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
import { validateCustomEventName } from "@calcom/core/event";
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -22,21 +22,14 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import type { PeriodType, SchedulingType } from "@calcom/prisma/enums";
|
||||
import type {
|
||||
BookerLayoutSettings,
|
||||
customInputSchema,
|
||||
EventTypeMetaDataSchema,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Form, showToast } from "@calcom/ui";
|
||||
|
||||
import type { AppProps } from "@lib/app-providers";
|
||||
|
||||
import type { AvailabilityOption } from "@components/eventtype/EventAvailabilityTab";
|
||||
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
|
||||
|
||||
import { type PageProps } from "~/event-types/views/event-types-single-view.getServerSideProps";
|
||||
@@ -84,73 +77,6 @@ const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/Manag
|
||||
|
||||
export type Host = { isFixed: boolean; userId: number; priority: number };
|
||||
|
||||
export type FormValues = {
|
||||
id: number;
|
||||
title: string;
|
||||
eventTitle: string;
|
||||
eventName: string;
|
||||
slug: string;
|
||||
isInstantEvent: boolean;
|
||||
length: number;
|
||||
offsetStart: number;
|
||||
description: string;
|
||||
disableGuests: boolean;
|
||||
lockTimeZoneToggleOnBookingPage: boolean;
|
||||
requiresConfirmation: boolean;
|
||||
requiresBookerEmailVerification: boolean;
|
||||
recurringEvent: RecurringEvent | null;
|
||||
schedulingType: SchedulingType | null;
|
||||
hidden: boolean;
|
||||
hideCalendarNotes: boolean;
|
||||
hashedLink: string | undefined;
|
||||
locations: {
|
||||
type: EventLocationType["type"];
|
||||
address?: string;
|
||||
attendeeAddress?: string;
|
||||
link?: string;
|
||||
hostPhoneNumber?: string;
|
||||
displayLocationPublicly?: boolean;
|
||||
phone?: string;
|
||||
hostDefault?: string;
|
||||
credentialId?: number;
|
||||
teamName?: string;
|
||||
}[];
|
||||
customInputs: CustomInputParsed[];
|
||||
schedule: number | null;
|
||||
periodType: PeriodType;
|
||||
periodDays: number;
|
||||
periodCountCalendarDays: boolean;
|
||||
periodDates: { startDate: Date; endDate: Date };
|
||||
seatsPerTimeSlot: number | null;
|
||||
seatsShowAttendees: boolean | null;
|
||||
seatsShowAvailabilityCount: boolean | null;
|
||||
seatsPerTimeSlotEnabled: boolean;
|
||||
scheduleName: string;
|
||||
minimumBookingNotice: number;
|
||||
minimumBookingNoticeInDurationType: number;
|
||||
beforeEventBuffer: number;
|
||||
afterEventBuffer: number;
|
||||
slotInterval: number | null;
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||
destinationCalendar: {
|
||||
integration: string;
|
||||
externalId: string;
|
||||
};
|
||||
successRedirectUrl: string;
|
||||
durationLimits?: IntervalLimit;
|
||||
bookingLimits?: IntervalLimit;
|
||||
onlyShowFirstAvailableSlot: boolean;
|
||||
children: ChildrenEventType[];
|
||||
hosts: Host[];
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
availability?: AvailabilityOption;
|
||||
bookerLayouts: BookerLayoutSettings;
|
||||
multipleDurationEnabled: boolean;
|
||||
users: EventTypeSetup["users"];
|
||||
assignAllTeamMembers: boolean;
|
||||
useEventTypeDestinationCalendarEmail: boolean;
|
||||
};
|
||||
|
||||
export type CustomInputParsed = typeof customInputSchema._output;
|
||||
|
||||
const querySchema = z.object({
|
||||
@@ -302,6 +228,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,
|
||||
@@ -436,7 +363,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
),
|
||||
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
||||
limits: <EventLimitsTab />,
|
||||
limits: <EventLimitsTab eventType={eventType} />,
|
||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
recurring: <EventRecurringTab eventType={eventType} />,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "3.8.4",
|
||||
"version": "3.8.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { DirectorySyncEvent, DirectorySyncRequest } from "@boxyhq/saml-jackson";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import handleGroupEvents from "@calcom/features/ee/dsync/lib/handleGroupEvents";
|
||||
import handleUserEvents from "@calcom/features/ee/dsync/lib/handleUserEvents";
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
// This is the handler for the SCIM API requests
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const { dsyncController } = await jackson();
|
||||
|
||||
const { method, query, body } = req;
|
||||
|
||||
const [directoryId, path, resourceId] = query.directory as string[];
|
||||
|
||||
// Handle the SCIM API requests
|
||||
const request: DirectorySyncRequest = {
|
||||
method: method as string,
|
||||
directoryId,
|
||||
resourceId,
|
||||
apiSecret: extractAuthToken(req),
|
||||
resourceType: path === "Users" ? "users" : "groups",
|
||||
body: body ? JSON.parse(body) : undefined,
|
||||
query: {
|
||||
count: req.query.count ? parseInt(req.query.count as string) : undefined,
|
||||
startIndex: req.query.startIndex ? parseInt(req.query.startIndex as string) : undefined,
|
||||
filter: req.query.filter as string,
|
||||
},
|
||||
};
|
||||
|
||||
const { status, data } = await dsyncController.requests.handle(request, handleEvents);
|
||||
|
||||
res.status(status).json(data);
|
||||
}
|
||||
|
||||
// Fetch the auth token from the request headers
|
||||
export const extractAuthToken = (req: NextApiRequest): string | null => {
|
||||
const authHeader = req.headers.authorization || null;
|
||||
|
||||
return authHeader ? authHeader.split(" ")[1] : null;
|
||||
};
|
||||
|
||||
// Handle the SCIM events
|
||||
const handleEvents = async (event: DirectorySyncEvent) => {
|
||||
const dSyncData = await prisma.dSyncData.findFirst({
|
||||
where: {
|
||||
directoryId: event.directory_id,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
organizationId: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!dSyncData) {
|
||||
throw new Error("Directory sync data not found");
|
||||
}
|
||||
|
||||
const { organizationId } = dSyncData;
|
||||
|
||||
if (!organizationId) {
|
||||
throw new Error(`Org ID not found for dsync ${dSyncData.id}`);
|
||||
}
|
||||
|
||||
if (event.event.includes("group")) {
|
||||
handleGroupEvents(event, organizationId);
|
||||
}
|
||||
|
||||
if (event.event === "user.created" || event.event === "user.updated") {
|
||||
await handleUserEvents(event, organizationId);
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createNextApiHandler } from "@calcom/trpc/server/createNextApiHandler";
|
||||
import { dsyncRouter } from "@calcom/trpc/server/routers/viewer/dsync/_router";
|
||||
|
||||
export default createNextApiHandler(dsyncRouter);
|
||||
@@ -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);
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter, usePathname } from "next/navigation";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
|
||||
import { BulkEditDefaultForEventsModal } from "@calcom/features/eventtypes/components/BulkEditDefaultForEventsModal";
|
||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { AvailabilitySliderTable } from "@calcom/features/timezone-buddy/components/AvailabilitySliderTable";
|
||||
@@ -21,6 +22,7 @@ import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||
|
||||
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
||||
const { t } = useLocale();
|
||||
const [bulkUpdateModal, setBulkUpdateModal] = useState(false);
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const meQuery = trpc.viewer.me.useQuery();
|
||||
@@ -65,6 +67,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
setBulkUpdateModal(true);
|
||||
},
|
||||
onError: (err) => {
|
||||
if (err instanceof HttpError) {
|
||||
@@ -74,6 +77,15 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
},
|
||||
});
|
||||
|
||||
const bulkUpdateDefaultAvailabilityMutation =
|
||||
trpc.viewer.availability.schedule.bulkUpdateToDefaultAvailability.useMutation({
|
||||
onSuccess: () => {
|
||||
utils.viewer.availability.list.invalidate();
|
||||
setBulkUpdateModal(false);
|
||||
showToast(t("success"), "success");
|
||||
},
|
||||
});
|
||||
|
||||
const duplicateMutation = trpc.viewer.availability.schedule.duplicate.useMutation({
|
||||
onSuccess: async ({ schedule }) => {
|
||||
await router.push(`/availability/${schedule.id}`);
|
||||
@@ -129,6 +141,14 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
{t("add_a_redirect")}
|
||||
</Link>
|
||||
</div>
|
||||
{bulkUpdateModal && (
|
||||
<BulkEditDefaultForEventsModal
|
||||
isPending={bulkUpdateDefaultAvailabilityMutation.isPending}
|
||||
open={bulkUpdateModal}
|
||||
setOpen={setBulkUpdateModal}
|
||||
bulkUpdateFunction={bulkUpdateDefaultAvailabilityMutation.mutate}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
||||
@@ -7,9 +7,9 @@ export type {
|
||||
CustomInputParsed,
|
||||
EventTypeSetup,
|
||||
EventTypeSetupProps,
|
||||
FormValues,
|
||||
Host,
|
||||
} from "~/event-types/views/event-types-single-view";
|
||||
export type { FormValues } from "@calcom/features/eventtypes/lib/types";
|
||||
|
||||
EventTypePageWrapper.PageWrapper = PageWrapper;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import DirectorySyncTeamView from "@calcom/features/ee/dsync/page/team-dsync-view";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const Page = DirectorySyncTeamView as CalPageWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,9 @@
|
||||
import PrivacyView from "@calcom/features/ee/organizations/pages/settings/privacy";
|
||||
|
||||
import type { CalPageWrapper } from "@components/PageWrapper";
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const Page = PrivacyView as CalPageWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -1,22 +1,63 @@
|
||||
"use client";
|
||||
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
||||
import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { WizardLayout } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
const querySchema = z.object({
|
||||
returnTo: z.string().optional(),
|
||||
slug: z.string().optional(),
|
||||
});
|
||||
|
||||
const CreateNewTeamPage = () => {
|
||||
const { t } = useLocale();
|
||||
const params = useParamsWithFallback();
|
||||
const parsedQuery = querySchema.safeParse(params);
|
||||
const router = useRouter();
|
||||
const telemetry = useTelemetry();
|
||||
|
||||
const isTeamBillingEnabledClient = !!process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY && HOSTED_CAL_FEATURES;
|
||||
const flag = isTeamBillingEnabledClient
|
||||
? {
|
||||
telemetryEvent: telemetryEventTypes.team_checkout_session_created,
|
||||
submitLabel: "checkout",
|
||||
}
|
||||
: {
|
||||
telemetryEvent: telemetryEventTypes.team_created,
|
||||
submitLabel: "continue",
|
||||
};
|
||||
|
||||
const returnToParam =
|
||||
(parsedQuery.success ? getSafeRedirectUrl(parsedQuery.data.returnTo) : "/teams") || "/teams";
|
||||
|
||||
const onSuccess = (data: RouterOutputs["viewer"]["teams"]["create"]) => {
|
||||
telemetry.event(flag.telemetryEvent);
|
||||
router.push(data.url);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>{t("create_new_team")}</title>
|
||||
<meta name="description" content={t("create_new_team_description")} />
|
||||
</Head>
|
||||
<CreateANewTeamForm />
|
||||
<CreateANewTeamForm
|
||||
slug={parsedQuery.success ? parsedQuery.data.slug : ""}
|
||||
onCancel={() => router.push(returnToParam)}
|
||||
submitLabel={flag.submitLabel}
|
||||
onSuccess={onSuccess}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import { useFlagMap } from "@calcom/features/flags/context/provider";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, URL_PROTOCOL_REGEX, IS_CALCOM, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||
import { pushGTMEvent } from "@calcom/lib/gtm";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -164,7 +165,6 @@ export default function Signup({
|
||||
const [premiumUsername, setPremiumUsername] = useState(false);
|
||||
const [usernameTaken, setUsernameTaken] = useState(false);
|
||||
const [isGoogleLoading, setIsGoogleLoading] = useState(false);
|
||||
|
||||
const searchParams = useCompatSearchParams();
|
||||
const telemetry = useTelemetry();
|
||||
const { t, i18n } = useLocale();
|
||||
@@ -223,6 +223,9 @@ export default function Signup({
|
||||
})
|
||||
.then(handleErrorsAndStripe)
|
||||
.then(async () => {
|
||||
if (process.env.NEXT_PUBLIC_GTM_ID)
|
||||
pushGTMEvent("create_account", { email: data.email, user: data.username, lang: data.language });
|
||||
|
||||
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
|
||||
const verifyOrGettingStarted = flags["email-verification"] ? "auth/verify-email" : "getting-started";
|
||||
const callBackUrl = `${
|
||||
|
||||
@@ -50,6 +50,8 @@ function TeamPage({
|
||||
const isBioEmpty = !team.bio || !team.bio.replace("<p><br></p>", "").length;
|
||||
const metadata = teamMetadataSchema.parse(team.metadata);
|
||||
|
||||
const teamOrOrgIsPrivate = team.isPrivate || (team?.parent?.isOrganization && team.parent?.isPrivate);
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.event(
|
||||
telemetryEventTypes.pageView,
|
||||
@@ -62,7 +64,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>
|
||||
@@ -158,7 +160,7 @@ function TeamPage({
|
||||
);
|
||||
|
||||
const profileImageSrc =
|
||||
isValidOrgDomain || team.metadata?.isOrganization
|
||||
isValidOrgDomain || team.isOrganization
|
||||
? getOrgAvatarUrl({ slug: currentOrgDomain, logoUrl: team.logoUrl })
|
||||
: getTeamAvatarUrl({ slug: team.slug, logoUrl: team.logoUrl, organizationId: team.parent?.id });
|
||||
|
||||
@@ -193,12 +195,18 @@ function TeamPage({
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{metadata?.isOrganization ? (
|
||||
<SubTeams />
|
||||
{team.isOrganization ? (
|
||||
!teamOrOrgIsPrivate ? (
|
||||
<SubTeams />
|
||||
) : (
|
||||
<div className="w-full text-center">
|
||||
<h2 className="text-emphasis font-semibold">{t("you_cannot_see_teams_of_org")}</h2>
|
||||
</div>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
{(showMembers.isOn || !team.eventTypes?.length) &&
|
||||
(team.isPrivate ? (
|
||||
(teamOrOrgIsPrivate ? (
|
||||
<div className="w-full text-center">
|
||||
<h2 data-testid="you-cannot-see-team-members" className="text-emphasis font-semibold">
|
||||
{t("you_cannot_see_team_members")}
|
||||
@@ -212,7 +220,7 @@ function TeamPage({
|
||||
<EventTypes eventTypes={team.eventTypes} />
|
||||
|
||||
{/* Hide "Book a team member button when team is private or hideBookATeamMember is true" */}
|
||||
{!team.hideBookATeamMember && !team.isPrivate && (
|
||||
{!team.hideBookATeamMember && !teamOrOrgIsPrivate && (
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
|
||||
@@ -25,6 +25,7 @@ export const getMultipleDurationValue = (
|
||||
if (multipleDurationConfig.includes(Number(queryDuration))) return Number(queryDuration);
|
||||
return defaultValue;
|
||||
};
|
||||
|
||||
export default function Type({
|
||||
slug,
|
||||
user,
|
||||
@@ -34,6 +35,7 @@ export default function Type({
|
||||
isBrandingHidden,
|
||||
eventData,
|
||||
isInstantMeeting,
|
||||
orgBannerUrl,
|
||||
}: PageProps) {
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -66,6 +68,7 @@ export default function Type({
|
||||
searchParams?.get("duration"),
|
||||
eventData.length
|
||||
)}
|
||||
orgBannerUrl={orgBannerUrl}
|
||||
/>
|
||||
</main>
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,15 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/client";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { createTeamEventType } from "./fixtures/users";
|
||||
import type { Fixtures } from "./lib/fixtures";
|
||||
import { test } from "./lib/fixtures";
|
||||
import { assertBookingVisibleFor, bookTeamEvent, bookUserEvent } from "./lib/testUtils";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
@@ -61,6 +67,137 @@ test.describe("Bookings", () => {
|
||||
secondUpcomingBooking.locator(`text=${bookingWhereFirstUserIsOrganizer!.title}`)
|
||||
).toBeVisible();
|
||||
});
|
||||
test("collective eventType booking should be visible to team admin", async ({ page, users }) => {
|
||||
const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users);
|
||||
|
||||
const { team: team1 } = await owner1.getFirstTeamMembership();
|
||||
const scenario = {
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
teamEventTitle: `collective-team-event`,
|
||||
teamEventSlug: slugify(`collective-team-event-${randomString(5)}`),
|
||||
};
|
||||
|
||||
const eventType = await createTeamEventType(owner1, team1, scenario);
|
||||
const { id: eventId, slug: evnetSlug } = eventType;
|
||||
|
||||
await prisma.host.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: commonUser.id,
|
||||
eventTypeId: eventId,
|
||||
isFixed: true,
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
eventTypeId: eventId,
|
||||
isFixed: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// to test if booking is visible to team admin/owner even if he is not part of the booking
|
||||
await prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: owner1.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
await bookTeamEvent(page, team1, eventType);
|
||||
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(owner1, page, eventType, true);
|
||||
// booking should not be visible for other team host
|
||||
await assertBookingVisibleFor(owner2, page, eventType, false);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, eventType, true);
|
||||
// booking should be visible for team1_teammate1 as he is part of the booking
|
||||
await assertBookingVisibleFor(team1_teammate1, page, eventType, true);
|
||||
});
|
||||
test("round-robin eventType booking should be visible to team admin", async ({ page, users }) => {
|
||||
const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users);
|
||||
|
||||
const { team: team1 } = await owner1.getFirstTeamMembership();
|
||||
const scenario = {
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
teamEventTitle: `round-robin-team-event`,
|
||||
teamEventSlug: slugify(`round-robin-team-event-${randomString(5)}`),
|
||||
};
|
||||
|
||||
const eventType = await createTeamEventType(owner1, team1, scenario);
|
||||
const { id: eventId } = eventType;
|
||||
|
||||
await prisma.host.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: commonUser.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: owner1.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
await bookTeamEvent(page, team1, eventType);
|
||||
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(owner1, page, eventType, true);
|
||||
// bookings should not be visible for other team host
|
||||
await assertBookingVisibleFor(owner2, page, eventType, false);
|
||||
});
|
||||
test("managed eventType booking should be visible to team admin", async ({ page, users }) => {
|
||||
const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users);
|
||||
|
||||
const { team: team1 } = await owner1.getFirstTeamMembership();
|
||||
|
||||
await owner1.apiLogin();
|
||||
await page.goto(`/event-types?dialog=new&eventPage=team%2F${team1.slug}&teamId=${team1.id}`);
|
||||
await page.getByTestId("managed-event-type").click();
|
||||
await page.getByTestId("event-type-quick-chat").click();
|
||||
await page.getByTestId("event-type-quick-chat").fill("managed-event");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("commonUser");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("team1_teammate1");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter");
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
|
||||
const eventType = await owner1.getFirstTeamEvent(team1.id);
|
||||
|
||||
await bookUserEvent(page, commonUser, eventType);
|
||||
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(owner1, page, eventType, true);
|
||||
// booking should not be visible for other team host
|
||||
await assertBookingVisibleFor(owner2, page, eventType, false);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, eventType, true);
|
||||
// booking should not be visible for team1_teammate1 as we booked for commonUser
|
||||
await assertBookingVisibleFor(team1_teammate1, page, eventType, false);
|
||||
});
|
||||
test("individual team members booking should not be visible to team admin", async ({ page, users }) => {
|
||||
const { owner1, owner2, commonUser, team1_teammate1 } = await createTeams(users);
|
||||
|
||||
const eventType = await commonUser.getFirstEventAsOwner();
|
||||
await bookUserEvent(page, commonUser, eventType);
|
||||
// non-team bookings should not be visible for the team host
|
||||
await assertBookingVisibleFor(owner1, page, eventType, false);
|
||||
// non-team bookings should not be visible for other team host
|
||||
await assertBookingVisibleFor(owner2, page, eventType, false);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, eventType, true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -111,3 +248,75 @@ async function createBooking({
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const createTeams = async (userFixture: Fixtures["users"]) => {
|
||||
const owner1 = await userFixture.create({ name: "team-owner-1" });
|
||||
const owner2 = await userFixture.create({ name: "team-owner-2" });
|
||||
const commonUser = await userFixture.create({ name: "commonUser" });
|
||||
const team1_teammate1 = await userFixture.create({ name: "team1_teammate1" });
|
||||
const team2_teammate1 = await userFixture.create({ name: "team2_teammate1" });
|
||||
const teamOne = await prisma.team.create({
|
||||
data: {
|
||||
name: "bookings-test-team-1",
|
||||
slug: slugify(`bookings-test-team-2-${randomString(5)}`),
|
||||
},
|
||||
});
|
||||
|
||||
const teamTwo = await prisma.team.create({
|
||||
data: {
|
||||
name: "bookings-test-team-2",
|
||||
slug: slugify(`bookings-test-team-2-${randomString(5)}`),
|
||||
},
|
||||
});
|
||||
|
||||
// create memberships
|
||||
await prisma.membership.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: owner1.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "OWNER",
|
||||
},
|
||||
{
|
||||
userId: commonUser.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: owner2.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "OWNER",
|
||||
},
|
||||
{
|
||||
userId: commonUser.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team2_teammate1.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
owner1,
|
||||
owner2,
|
||||
commonUser,
|
||||
team1_teammate1,
|
||||
team2_teammate1,
|
||||
teamOne,
|
||||
teamTwo,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -107,6 +107,10 @@ testBothFutureAndLegacyRoutes.describe("Event Types tests", () => {
|
||||
|
||||
expect(formTitle).toBe(firstTitle);
|
||||
expect(formSlug).toContain(firstSlug);
|
||||
|
||||
const test = await page.getByTestId("continue").click();
|
||||
const toast = await page.waitForSelector('[data-testid="toast-success"]');
|
||||
expect(toast).toBeTruthy();
|
||||
});
|
||||
|
||||
test("edit first event", async ({ page }) => {
|
||||
|
||||
@@ -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");
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ const createTeamWorkflow = async (user: { id: number }, team: { id: number }) =>
|
||||
});
|
||||
};
|
||||
|
||||
const createTeamEventType = async (
|
||||
export const createTeamEventType = async (
|
||||
user: { id: number },
|
||||
team: { id: number },
|
||||
scenario?: {
|
||||
@@ -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({
|
||||
@@ -669,6 +688,7 @@ type CustomUserOpts = Partial<Pick<Prisma.User, CustomUserOptsKeys>> & {
|
||||
roleInOrganization?: MembershipRole;
|
||||
schedule?: Schedule;
|
||||
password?: string | null;
|
||||
emailDomain?: string;
|
||||
};
|
||||
|
||||
// creates the actual user in the db.
|
||||
@@ -686,10 +706,11 @@ 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`,
|
||||
email: opts?.email ?? `${uname}@${emailDomain}`,
|
||||
password: {
|
||||
create: {
|
||||
hash: hashPassword(uname),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { Frame, Page } from "@playwright/test";
|
||||
import type { APIResponse } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import { createHash } from "crypto";
|
||||
import EventEmitter from "events";
|
||||
@@ -369,3 +370,53 @@ export async function doOnOrgDomain(
|
||||
// When App directory is there, this is the 404 page text. We should work on fixing the 404 page as it changed due to app directory.
|
||||
export const NotFoundPageTextAppDir = "This page does not exist.";
|
||||
// export const NotFoundPageText = "ERROR 404";
|
||||
|
||||
export type Team = {
|
||||
id: number;
|
||||
slug: string | null;
|
||||
name: string;
|
||||
};
|
||||
|
||||
export type EventType = {
|
||||
id: number;
|
||||
title: string;
|
||||
slug: string;
|
||||
};
|
||||
export type User = {
|
||||
id: number;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
};
|
||||
|
||||
export async function bookTeamEvent(page: Page, team: Team, eventType: EventType) {
|
||||
await page.goto(`/team/${team.slug}/${eventType.slug}/`);
|
||||
await bookEventOnThisPage(page);
|
||||
const bookingTitle = `${eventType.title} between ${team.name} and ${testName}`;
|
||||
await assertBookingIsCorrect(page, bookingTitle);
|
||||
}
|
||||
export async function bookUserEvent(page: Page, user: User, eventType: EventType) {
|
||||
await page.goto(`/${user.username}/${eventType.slug}/`);
|
||||
await bookEventOnThisPage(page);
|
||||
const bookingTitle = `${eventType.title} between ${user.name} and ${testName}`;
|
||||
await assertBookingIsCorrect(page, bookingTitle);
|
||||
}
|
||||
|
||||
export async function assertBookingIsCorrect(page: Page, bookingTitle: string) {
|
||||
await expect(page.locator("[data-testid=booking-title]")).toHaveText(bookingTitle);
|
||||
// The booker should be in the attendee list
|
||||
await expect(page.locator(`[data-testid="attendee-name-${testName}"]`)).toHaveText(testName);
|
||||
}
|
||||
|
||||
export const assertBookingVisibleFor = async (
|
||||
user: { apiLogin: () => Promise<APIResponse> },
|
||||
pageFixture: Fixtures["page"],
|
||||
eventType: EventType,
|
||||
shouldBeVisible: boolean
|
||||
) => {
|
||||
await user.apiLogin();
|
||||
await pageFixture.goto(`/bookings/upcoming`);
|
||||
const upcomingBookingList = pageFixture.locator('[data-testid="booking-item"]');
|
||||
shouldBeVisible
|
||||
? await expect(upcomingBookingList.locator(`text=${eventType.title}`)).toBeVisible()
|
||||
: await expect(upcomingBookingList.locator(`text=${eventType.title}`)).toBeHidden();
|
||||
};
|
||||
|
||||
@@ -11,9 +11,9 @@ import {
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
} from "./lib/testUtils";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
test.afterAll(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Managed Event Types tests", () => {
|
||||
test.describe("Managed Event Types", () => {
|
||||
test("Can create managed event type", async ({ page, users }) => {
|
||||
// Creating the owner user of the team
|
||||
const adminUser = await users.create();
|
||||
@@ -61,7 +61,7 @@ test.describe("Managed Event Types tests", () => {
|
||||
await expect(page.locator('input[name="title"]')).toBeEditable();
|
||||
await expect(page.locator('input[name="slug"]')).toBeEditable();
|
||||
await expect(page.locator('input[name="length"]')).toBeEditable();
|
||||
await adminUser.logout();
|
||||
await adminUser.apiLogin();
|
||||
});
|
||||
|
||||
await test.step("Managed event type exists for added member", async () => {
|
||||
@@ -73,7 +73,7 @@ test.describe("Managed Event Types tests", () => {
|
||||
await page.locator('button[data-testid^="accept-invitation"]').click();
|
||||
await page.getByText("Member").waitFor();
|
||||
|
||||
await memberUser.logout();
|
||||
await page.goto("/auth/logout");
|
||||
|
||||
// Coming back as team owner to assign member user to managed event
|
||||
await adminUser.apiLogin();
|
||||
@@ -107,7 +107,6 @@ test.describe("Managed Event Types tests", () => {
|
||||
|
||||
await test.step("Managed event type has locked fields for added member", async () => {
|
||||
await adminUser.logout();
|
||||
|
||||
// Coming back as member user to see if there is a managed event present after assignment
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
@@ -118,6 +117,90 @@ test.describe("Managed Event Types tests", () => {
|
||||
await expect(page.locator('input[name="title"]')).not.toBeEditable();
|
||||
await expect(page.locator('input[name="slug"]')).not.toBeEditable();
|
||||
await expect(page.locator('input[name="length"]')).not.toBeEditable();
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
});
|
||||
|
||||
await test.step("Managed event type provides discrete field lock/unlock state for admin", async () => {
|
||||
await adminUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
// Locked by default
|
||||
const titleLockIndicator = page.getByTestId("locked-indicator-title");
|
||||
await expect(titleLockIndicator).toBeVisible();
|
||||
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(1);
|
||||
|
||||
// Proceed to unlock and check that it got unlocked
|
||||
titleLockIndicator.click();
|
||||
await expect(titleLockIndicator.locator("[data-state='checked']")).toHaveCount(0);
|
||||
await expect(titleLockIndicator.locator("[data-state='unchecked']")).toHaveCount(1);
|
||||
|
||||
// Save changes
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
});
|
||||
|
||||
await test.step("Managed event type shows discretionally unlocked field to member", async () => {
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
await expect(page.locator('input[name="title"]')).toBeEditable();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
});
|
||||
|
||||
await test.step("Managed event type should only update the unlocked fields modified by Admin", async () => {
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
await expect(page.locator('input[name="title"]')).toBeEditable();
|
||||
await page.locator('input[name="title"]').fill(`Managed Event Title`);
|
||||
// Save changes
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
|
||||
await adminUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
await page.getByTestId("event-types").locator('a[title="managed"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
await page.locator('input[name="length"]').fill(`45`);
|
||||
// Save changes
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
await page.goto("/auth/logout");
|
||||
|
||||
await memberUser.apiLogin();
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId("event-types").locator('a[title="Managed Event Title"]').click();
|
||||
await page.waitForURL("event-types/**");
|
||||
|
||||
//match length
|
||||
expect(await page.locator("[data-testid=duration]").getAttribute("value")).toBe("45");
|
||||
//ensure description didn't update
|
||||
expect(await page.locator(`input[name="title"]`).getAttribute("value")).toBe(`Managed Event Title`);
|
||||
await page.locator('input[name="title"]').fill(`managed`);
|
||||
// Save changes
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.getByTestId("toast-success").waitFor();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||
|
||||
await page.goto("/event-types");
|
||||
await bookingPage.goToEventType(teamEventTitle);
|
||||
await page.getByTestId("location-select").click();
|
||||
await page.locator(`text="Cal Video (Global)"`).click();
|
||||
await bookingPage.goToTab("event_advanced_tab_title");
|
||||
});
|
||||
|
||||
@@ -19,7 +21,6 @@ test.describe("Check advanced options in a managed team event type", () => {
|
||||
await bookingPage.checkRequiresBookerEmailVerification();
|
||||
await bookingPage.checkHideNotes();
|
||||
await bookingPage.checkRedirectOnBooking();
|
||||
await bookingPage.checkEnablePrivateUrl();
|
||||
await bookingPage.checkLockTimezone();
|
||||
await bookingPage.updateEventType();
|
||||
await bookingPage.goToEventTypesPage();
|
||||
@@ -32,7 +33,6 @@ test.describe("Check advanced options in a managed team event type", () => {
|
||||
await bookingPage.checkRequiresBookerEmailVerification();
|
||||
await bookingPage.checkHideNotes();
|
||||
await bookingPage.checkRedirectOnBooking();
|
||||
await bookingPage.checkEnablePrivateUrl();
|
||||
await bookingPage.toggleOfferSeats();
|
||||
await bookingPage.checkLockTimezone();
|
||||
await bookingPage.updateEventType();
|
||||
|
||||
@@ -0,0 +1,391 @@
|
||||
import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { createTeamEventType } from "../fixtures/users";
|
||||
import { test } from "../lib/fixtures";
|
||||
import type { Fixtures } from "../lib/fixtures";
|
||||
import {
|
||||
assertBookingVisibleFor,
|
||||
bookTimeSlot,
|
||||
doOnOrgDomain,
|
||||
NotFoundPageTextAppDir,
|
||||
selectFirstAvailableTimeSlotNextMonth,
|
||||
testName,
|
||||
} from "../lib/testUtils";
|
||||
|
||||
test.describe("Bookings list for organizations", () => {
|
||||
test.afterEach(({ orgs, users }) => {
|
||||
orgs.deleteAll();
|
||||
users.deleteAll();
|
||||
});
|
||||
|
||||
test.describe("Team Event", () => {
|
||||
test("Collective EventType booking list", async ({ page, users, orgs }) => {
|
||||
const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } =
|
||||
await createOrg(orgs, users);
|
||||
|
||||
const { team: team1 } = await team1owner.getFirstTeamMembership();
|
||||
const scenario = {
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
teamEventTitle: `collective-team-event`,
|
||||
teamEventSlug: slugify(`collective-team-event-${randomString(5)}`),
|
||||
};
|
||||
const eventType = await createTeamEventType(team1owner, team1, scenario);
|
||||
const { id: eventId, slug: evnetSlug } = eventType;
|
||||
|
||||
await prisma.host.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: commonUser.id,
|
||||
eventTypeId: eventId,
|
||||
isFixed: true,
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
eventTypeId: eventId,
|
||||
isFixed: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
// to test if booking is visible to team admin/owner even if he is not part of the booking
|
||||
await prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: team1owner.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
await expectPageToBeNotFound({ page, url: `/team/${team1.slug}/${eventType.slug}` });
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await bookTeamEvent({ page, team: team1, event: eventType });
|
||||
}
|
||||
);
|
||||
// booking should be visible for the Org OWNER even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgOwner, page, eventType, true);
|
||||
// booking should be visible for the Org ADMIN even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgAdmin, page, eventType, true);
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(team1owner, page, eventType, true);
|
||||
// booking should not be visible for other team host, because its a team1 team-event
|
||||
await assertBookingVisibleFor(team2owner, page, eventType, false);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, eventType, true);
|
||||
// booking should be visible for team1_teammate1 as he is part of the booking
|
||||
await assertBookingVisibleFor(team1_teammate1, page, eventType, true);
|
||||
});
|
||||
|
||||
test("Round Robin EventType booking list", async ({ page, users, orgs }) => {
|
||||
const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } =
|
||||
await createOrg(orgs, users);
|
||||
|
||||
const { team: team1 } = await team1owner.getFirstTeamMembership();
|
||||
const scenario = {
|
||||
schedulingType: SchedulingType.ROUND_ROBIN,
|
||||
teamEventTitle: `round-robin-team-event`,
|
||||
teamEventSlug: slugify(`round-robin-team-event-${randomString(5)}`),
|
||||
};
|
||||
|
||||
const eventType = await createTeamEventType(team1owner, team1, scenario);
|
||||
const { id: eventId } = eventType;
|
||||
await prisma.host.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: commonUser.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.host.deleteMany({
|
||||
where: {
|
||||
userId: team1owner.id,
|
||||
eventTypeId: eventId,
|
||||
},
|
||||
});
|
||||
|
||||
await expectPageToBeNotFound({ page, url: `/team/${team1.slug}/${eventType.slug}` });
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await bookTeamEvent({ page, team: team1, event: eventType });
|
||||
}
|
||||
);
|
||||
// booking should be visible for the Org OWNER even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgOwner, page, eventType, true);
|
||||
// booking should be visible for the Org ADMIN even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgAdmin, page, eventType, true);
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(team1owner, page, eventType, true);
|
||||
// booking should not be visible for other team host
|
||||
await assertBookingVisibleFor(team2owner, page, eventType, false);
|
||||
});
|
||||
|
||||
test("Managed EventType booking list", async ({ page, users, orgs }) => {
|
||||
const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } =
|
||||
await createOrg(orgs, users);
|
||||
|
||||
const { team: team1 } = await team1owner.getFirstTeamMembership();
|
||||
|
||||
await team1owner.apiLogin();
|
||||
await page.goto(`/event-types?dialog=new&eventPage=team%2F${team1.slug}&teamId=${team1.id}`);
|
||||
await page.getByTestId("managed-event-type").click();
|
||||
await page.getByTestId("event-type-quick-chat").click();
|
||||
await page.getByTestId("event-type-quick-chat").fill("managed-event");
|
||||
await page.getByRole("button", { name: "Continue" }).click();
|
||||
await page.getByTestId("vertical-tab-assignment").click();
|
||||
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("commonUser");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).fill("team1_teammate1");
|
||||
await page.getByRole("combobox", { name: "assignment-dropdown" }).press("Enter");
|
||||
await page.getByTestId("update-eventtype").click();
|
||||
|
||||
const eventType = await team1owner.getFirstTeamEvent(team1.id);
|
||||
|
||||
await expectPageToBeNotFound({ page, url: `/${commonUser.username}/${eventType.slug}` });
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await bookUserEvent({ page, user: commonUser, event: eventType });
|
||||
}
|
||||
);
|
||||
|
||||
// booking should be visible for the Org OWNER even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgOwner, page, eventType, true);
|
||||
// booking should be visible for the Org ADMIN even though he is not part of the booking
|
||||
await assertBookingVisibleFor(orgAdmin, page, eventType, true);
|
||||
// booking should be visible for the team host even though he is not part of the booking
|
||||
await assertBookingVisibleFor(team1owner, page, eventType, true);
|
||||
// booking should not be visible for other team host
|
||||
await assertBookingVisibleFor(team2owner, page, eventType, false);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("User Event", () => {
|
||||
test("User personal booking list", async ({ page, users, orgs }) => {
|
||||
const { org, orgOwner, orgAdmin, team1owner, team2owner, commonUser, team1_teammate1 } =
|
||||
await createOrg(orgs, users);
|
||||
|
||||
const event = await commonUser.getFirstEventAsOwner();
|
||||
|
||||
await page.goto(`/${commonUser.username}/${event.slug}`);
|
||||
// Shouldn't be servable on the non-org domain
|
||||
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
|
||||
|
||||
await doOnOrgDomain(
|
||||
{
|
||||
orgSlug: org.slug,
|
||||
page,
|
||||
},
|
||||
async () => {
|
||||
await bookUserEvent({ page, user: commonUser, event });
|
||||
}
|
||||
);
|
||||
// booking should be visible for the Org OWNER
|
||||
await assertBookingVisibleFor(orgOwner, page, event, true);
|
||||
// booking should be visible for the Org ADMIN
|
||||
await assertBookingVisibleFor(orgAdmin, page, event, true);
|
||||
// booking should be visible for the team host
|
||||
await assertBookingVisibleFor(team1owner, page, event, true);
|
||||
// booking should be visible for other team host because commonUser is part of both teams
|
||||
await assertBookingVisibleFor(team2owner, page, event, true);
|
||||
// booking should be visible for commonUser as he is part of the booking
|
||||
await assertBookingVisibleFor(commonUser, page, event, true);
|
||||
// booking should not be visible for team1_teammate1
|
||||
await assertBookingVisibleFor(team1_teammate1, page, event, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function bookUserEvent({
|
||||
page,
|
||||
user,
|
||||
event,
|
||||
}: {
|
||||
page: Page;
|
||||
user: {
|
||||
username: string | null;
|
||||
name: string | null;
|
||||
};
|
||||
event: { slug: string; title: string };
|
||||
}) {
|
||||
await page.goto(`/${user.username}/${event.slug}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
// The title of the booking
|
||||
const BookingTitle = `${event.title} between ${user.name} and ${testName}`;
|
||||
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
|
||||
// The booker should be in the attendee list
|
||||
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
|
||||
}
|
||||
|
||||
async function bookTeamEvent({
|
||||
page,
|
||||
team,
|
||||
event,
|
||||
}: {
|
||||
page: Page;
|
||||
team: {
|
||||
slug: string | null;
|
||||
name: string | null;
|
||||
};
|
||||
event: { slug: string; title: string };
|
||||
}) {
|
||||
// Note that even though the default way to access a team booking in an organization is to not use /team in the URL, but it isn't testable with playwright as the rewrite is taken care of by Next.js config which can't handle on the fly org slug's handling
|
||||
// So, we are using /team in the URL to access the team booking
|
||||
// There are separate tests to verify that the next.config.js rewrites are working
|
||||
// Also there are additional checkly tests that verify absolute e2e flow. They are in __checks__/organization.spec.ts
|
||||
await page.goto(`/team/${team.slug}/${event.slug}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await bookTimeSlot(page);
|
||||
await expect(page.getByTestId("success-page")).toBeVisible();
|
||||
|
||||
// The title of the booking
|
||||
const BookingTitle = `${event.title} between ${team.name} and ${testName}`;
|
||||
await expect(page.getByTestId("booking-title")).toHaveText(BookingTitle);
|
||||
// The booker should be in the attendee list
|
||||
await expect(page.getByTestId(`attendee-name-${testName}`)).toHaveText(testName);
|
||||
}
|
||||
|
||||
async function expectPageToBeNotFound({ page, url }: { page: Page; url: string }) {
|
||||
await page.goto(`${url}`);
|
||||
await expect(page.locator(`text=${NotFoundPageTextAppDir}`)).toBeVisible();
|
||||
}
|
||||
|
||||
const createOrg = async (orgs: Fixtures["orgs"], userFixture: Fixtures["users"]) => {
|
||||
const org = await orgs.create({
|
||||
name: "TestOrg",
|
||||
});
|
||||
const orgOwner = await userFixture.create({
|
||||
name: "org-owner",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.OWNER,
|
||||
});
|
||||
|
||||
const orgAdmin = await userFixture.create({
|
||||
name: "org-admin",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.ADMIN,
|
||||
});
|
||||
const team1owner = await userFixture.create({
|
||||
name: "team-owner-1",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const team2owner = await userFixture.create({
|
||||
name: "team-owner-2",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const commonUser = await userFixture.create({
|
||||
name: "commonUser",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const team1_teammate1 = await userFixture.create({
|
||||
name: "team1_teammate1",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const team2_teammate1 = await userFixture.create({
|
||||
name: "team2_teammate1",
|
||||
organizationId: org.id,
|
||||
roleInOrganization: MembershipRole.MEMBER,
|
||||
});
|
||||
const teamOne = await prisma.team.create({
|
||||
data: {
|
||||
name: "bookings-test-team-1",
|
||||
slug: slugify(`bookings-test-team-2-${randomString(5)}`),
|
||||
parentId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
const teamTwo = await prisma.team.create({
|
||||
data: {
|
||||
name: "bookings-test-team-2",
|
||||
slug: slugify(`bookings-test-team-2-${randomString(5)}`),
|
||||
parentId: org.id,
|
||||
},
|
||||
});
|
||||
|
||||
// create memberships
|
||||
await prisma.membership.createMany({
|
||||
data: [
|
||||
{
|
||||
userId: team1owner.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "OWNER",
|
||||
},
|
||||
{
|
||||
userId: commonUser.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team1_teammate1.id,
|
||||
teamId: teamOne.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team2owner.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "OWNER",
|
||||
},
|
||||
{
|
||||
userId: commonUser.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
{
|
||||
userId: team2_teammate1.id,
|
||||
teamId: teamTwo.id,
|
||||
accepted: true,
|
||||
role: "MEMBER",
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
org,
|
||||
orgOwner,
|
||||
orgAdmin,
|
||||
team1owner,
|
||||
team2owner,
|
||||
commonUser,
|
||||
team1_teammate1,
|
||||
team2_teammate1,
|
||||
teamOne,
|
||||
teamTwo,
|
||||
};
|
||||
};
|
||||
@@ -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,17 +142,16 @@ test.describe("Update Profile", () => {
|
||||
|
||||
await page.goto(verifyUrl);
|
||||
|
||||
const successLocator = await page.waitForSelector('[data-testId="toast-success"]');
|
||||
|
||||
expect(await successLocator.textContent()).toContain(email);
|
||||
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");
|
||||
|
||||
await page.goto("/settings/my-account/profile");
|
||||
const emailInputUpdated = await 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
|
||||
@@ -166,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);
|
||||
|
||||
@@ -176,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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,17 +33,16 @@ test.describe("UploadAvatar", async () => {
|
||||
|
||||
const response = await prisma.avatar.findUniqueOrThrow({
|
||||
where: {
|
||||
teamId_userId: {
|
||||
teamId_userId_isBanner: {
|
||||
userId: user.id,
|
||||
teamId: 0,
|
||||
isBanner: false,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// todo: remove this; ideally the organization-avatar is updated the moment
|
||||
// 'Settings updated succesfully' is saved.
|
||||
await page.waitForLoadState("networkidle");
|
||||
const avatar = page.getByTestId("profile-upload-avatar").locator("img");
|
||||
// Wait for the avatar image with the http src to appear instead of the data uri
|
||||
const avatar = page.getByTestId("profile-upload-avatar").locator("img[src^=http]");
|
||||
|
||||
const src = await avatar.getAttribute("src");
|
||||
|
||||
|
||||
@@ -275,6 +275,44 @@ testBothFutureAndLegacyRoutes.describe("Teams - NonOrg", (routeVariant) => {
|
||||
await expect(page.locator('[data-testid="you-cannot-see-team-members"]')).toBeVisible();
|
||||
await expect(page.locator('[data-testid="team-members-container"]')).toBeHidden();
|
||||
});
|
||||
test("Email Embeds slots are loading for team event types", async ({ page, users }) => {
|
||||
const teamMatesObj = [
|
||||
{ name: "teammate-1" },
|
||||
{ name: "teammate-2" },
|
||||
{ name: "teammate-3" },
|
||||
{ name: "teammate-4" },
|
||||
];
|
||||
|
||||
const owner = await users.create(
|
||||
{ username: "pro-user", name: "pro-user" },
|
||||
{
|
||||
hasTeam: true,
|
||||
teammates: teamMatesObj,
|
||||
schedulingType: SchedulingType.COLLECTIVE,
|
||||
}
|
||||
);
|
||||
|
||||
await owner.apiLogin();
|
||||
const { team } = await owner.getFirstTeamMembership();
|
||||
const {
|
||||
title: teamEventTitle,
|
||||
slug: teamEventSlug,
|
||||
id: teamEventId,
|
||||
} = await owner.getFirstTeamEvent(team.id);
|
||||
|
||||
await page.goto("/event-types");
|
||||
|
||||
await page.getByTestId(`event-type-options-${teamEventId}`).first().click();
|
||||
await page.getByTestId("embed").click();
|
||||
await page.getByTestId("email").click();
|
||||
await page.getByTestId("incrementMonth").click();
|
||||
|
||||
await expect(page.getByTestId("no-slots-available")).toBeHidden();
|
||||
|
||||
// Check Team Url
|
||||
const availableTimesUrl = await page.getByTestId("see_all_available_times").getAttribute("href");
|
||||
await expect(availableTimesUrl).toContain(`/team/${team.slug}/${teamEventSlug}`);
|
||||
});
|
||||
|
||||
todo("Create a Round Robin with different leastRecentlyBooked hosts");
|
||||
todo("Reschedule a Collective EventType booking");
|
||||
|
||||
+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);
|
||||
});
|
||||
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "يجب أن يكون لديك جدول واحد على الأقل",
|
||||
"default_conferencing_bulk_description": "تحديث المواقع لأنواع الأحداث المحددة",
|
||||
"locked_for_members": "مقفل للأعضاء",
|
||||
"locked_apps_description": "سيتمكن الأعضاء من رؤية التطبيقات النشطة، ولكن لن يتمكنوا من تعديل أي إعدادات للتطبيق",
|
||||
"locked_webhooks_description": "سيكون الأعضاء قادرين على رؤية webhooks النشطة، ولكن من دون القدرة على تعديل أي إعدادات Webhook",
|
||||
"locked_workflows_description": "سيكون الأعضاء قادرين على رؤية مسارات العمل النشطة، ولكن من دون القدرة على تعديل أي إعدادات لمسار العمل",
|
||||
"locked_by_admin": "مقفل من قبل المشرف",
|
||||
"apps_locked_for_members_description": "سيتمكن الأعضاء من رؤية التطبيقات النشطة، ولكن لن يتمكنوا من تعديل أي إعدادات للتطبيق",
|
||||
"locked_by_team_admin": "مقفل من قبل المشرف",
|
||||
"app_not_connected": "أنت غير متصل بحساب {{appName}}.",
|
||||
"connect_now": "اتصل الآن",
|
||||
"managed_event_dialog_confirm_button_one": "الاستبدال وإخطار {{count}} عضو",
|
||||
@@ -2061,6 +2059,7 @@
|
||||
"workflow_automation": "أتمتة سير العمل",
|
||||
"scheduling_for_your_team": "أتمتة سير العمل",
|
||||
"no_members_found": "لم يُعثر على أعضاء",
|
||||
"directory_sync": "مزامنة الدليل",
|
||||
"event_setup_length_error": "إعداد الفعالية: يجب أن تكون المدة لدقيقة على الأقل.",
|
||||
"availability_schedules": "جدولة التوافر",
|
||||
"unauthorized": "غير مصرح به",
|
||||
@@ -2097,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "عملية انضمام ودعم هندسي مخصصين",
|
||||
"need_help": "هل تحتاج إلى مساعدة؟",
|
||||
"show_more": "إظهار المزيد",
|
||||
"privacy": "الخصوصية",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
{}
|
||||
{}
|
||||
@@ -1 +1 @@
|
||||
{}
|
||||
{}
|
||||
@@ -21,4 +21,4 @@
|
||||
"load_more_results": "Carrega més resultats",
|
||||
"integration_meeting_id": "{{integrationName}} Identificador de la reunió: {{meetingId}}",
|
||||
"confirmed_event_type_subject": "Confirmat: {{eventType}} amb {{name}} el {{date}}"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "Musíte mít alespoň jeden rozvrh",
|
||||
"default_conferencing_bulk_description": "Aktualizujte místa pro vybrané typy událostí",
|
||||
"locked_for_members": "Zamčeno pro členy",
|
||||
"locked_apps_description": "Členové uvidí aktivní aplikace, ale nastavení aplikací upravovat nemohou",
|
||||
"locked_webhooks_description": "Členové uvidí aktivní webhooky, ale nastavení webhooků upravovat nemohou",
|
||||
"locked_workflows_description": "Členové uvidí aktivní pracovní postupy, ale nastavení pracovních postupů upravovat nemohou",
|
||||
"locked_by_admin": "Uzamčeno správcem týmu",
|
||||
"apps_locked_for_members_description": "Členové uvidí aktivní aplikace, ale nastavení aplikací upravovat nemohou",
|
||||
"locked_by_team_admin": "Uzamčeno správcem týmu",
|
||||
"app_not_connected": "Nemáte připojený účet {{appName}}.",
|
||||
"connect_now": "Připojte se",
|
||||
"managed_event_dialog_confirm_button_one": "Nahradit a upozornit {{count}} člena",
|
||||
@@ -2061,6 +2059,7 @@
|
||||
"workflow_automation": "Automatizace pracovních postupů",
|
||||
"scheduling_for_your_team": "Automatizace pracovních postupů",
|
||||
"no_members_found": "Nenalezeni žádní členové",
|
||||
"directory_sync": "Synchronizace adresářů",
|
||||
"event_setup_length_error": "Nastavení události: Doba trvání musí být alespoň 1 minuta.",
|
||||
"availability_schedules": "Plány dostupnosti",
|
||||
"unauthorized": "Neautorizováno",
|
||||
@@ -2097,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Vyhrazená podpora zaškolovací a inženýrská podpora",
|
||||
"need_help": "Potřebujete pomoc?",
|
||||
"show_more": "Zobrazit více",
|
||||
"privacy": "Ochrana soukromí",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1535,4 +1535,4 @@
|
||||
"confirm_your_details": "Bekræft dine oplysninger",
|
||||
"overlay_my_calendar": "Vis min kalender",
|
||||
"need_help": "Brug for hjælp?"
|
||||
}
|
||||
}
|
||||
@@ -808,6 +808,7 @@
|
||||
"label": "Bezeichnung",
|
||||
"placeholder": "Platzhalter",
|
||||
"display_add_to_calendar_organizer": "„Zum Kalender hinzufügen“-E-Mail als Organisator anzeigen",
|
||||
"reconnect_calendar_to_use": "Bitte beachten Sie, dass Sie möglicherweise die Verbindung trennen und anschließend Ihr „Zum Kalender hinzufügen“-Konto erneut verbinden müssen, um diese Funktion nutzen zu können.",
|
||||
"type": "Typ",
|
||||
"edit": "Bearbeiten",
|
||||
"add_input": "Eingabe hinzufügen",
|
||||
@@ -1474,6 +1475,8 @@
|
||||
"copy_link_to_form": "Link in Formular kopieren",
|
||||
"theme": "Theme",
|
||||
"theme_applies_note": "Dies gilt nur für Ihre öffentlichen Buchungsseiten",
|
||||
"app_theme": "Dashboard-Theme",
|
||||
"app_theme_applies_note": "Dies gilt nur für Ihr eingeloggtes Dashboard",
|
||||
"theme_system": "Systemstandard",
|
||||
"add_a_team": "Team hinzufügen",
|
||||
"add_webhook_description": "Erhalten Sie Meetingdaten in Echtzeit, wenn etwas in {{appName}} passiert",
|
||||
@@ -1599,6 +1602,7 @@
|
||||
"attendee_email_info": "Die E-Mail-Adresse der buchenden Person",
|
||||
"kbar_search_placeholder": "Geben Sie einen Befehl ein oder suchen Sie ...",
|
||||
"invalid_credential": "Oh nein! Es sieht so aus, als ob die Berechtigung abgelaufen ist oder entzogen wurden. Bitte erneut installieren.",
|
||||
"invalid_credential_action": "App neu installieren",
|
||||
"reschedule_reason": "Verlegungsgrund",
|
||||
"choose_common_schedule_team_event": "Gemeinsamen Zeitplan auswählen",
|
||||
"choose_common_schedule_team_event_description": "Aktivieren Sie dies, wenn Sie einen gemeinsamen Zeitplan zwischen Hosts verwenden möchten. Wenn deaktiviert, ist jeder Host basierend auf seinen Standardplan buchbar.",
|
||||
@@ -1884,10 +1888,8 @@
|
||||
"requires_at_least_one_schedule": "Sie müssen mindestens einen Verfügbarkeitsplan haben",
|
||||
"default_conferencing_bulk_description": "Orte für die ausgewählten Termintypen aktualisieren",
|
||||
"locked_for_members": "Für Mitglieder gesperrt",
|
||||
"locked_apps_description": "Mitglieder können die aktiven Apps sehen, können aber keine App-Einstellungen anpassen",
|
||||
"locked_webhooks_description": "Mitglieder können die aktiven Webhooks sehen, können aber keine Webhook-Einstellungen anpassen",
|
||||
"locked_workflows_description": "Mitglieder können die aktiven Workflows sehen, können aber keine Workflow-Einstellungen anpassen",
|
||||
"locked_by_admin": "Vom Team-Admin gesperrt",
|
||||
"apps_locked_for_members_description": "Mitglieder können die aktiven Apps sehen, können aber keine App-Einstellungen anpassen",
|
||||
"locked_by_team_admin": "Vom Team-Admin gesperrt",
|
||||
"app_not_connected": "Sie haben kein {{appName}}-Konto verbunden.",
|
||||
"connect_now": "Jetzt verbinden",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} Mitglied ersetzen & benachrichtigen",
|
||||
@@ -2136,6 +2138,7 @@
|
||||
"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",
|
||||
"directory_sync": "Verzeichnissynchronisierung",
|
||||
"event_setup_length_error": "Ereignis-Einrichtung: Die Dauer muss mindestens 1 Minute betragen.",
|
||||
"availability_schedules": "Verfügbarkeitspläne",
|
||||
"unauthorized": "Nicht authorisiert",
|
||||
@@ -2262,5 +2265,13 @@
|
||||
"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",
|
||||
"privacy": "Datenschutz",
|
||||
"primary": "Primär",
|
||||
"make_primary": "Zur Primäradresse machen",
|
||||
"add_email": "E-Mail-Adresse hinzufügen",
|
||||
"add_email_description": "Fügen Sie eine E-Mail-Adresse hinzu, um die Ihre primäre E-Mail-Adresse ersetzt, oder als alternative E-Mail in Ihren Ereignistypen verwendet wird.",
|
||||
"confirm_email": "Bestätigen Sie Ihre E-Mail-Adresse",
|
||||
"confirm_email_description": "Wir haben eine E-Mail an <strong>{{email}}</strong> gesendet. Klicken Sie auf den Link in der E-Mail, um diese Adresse zu bestätigen.",
|
||||
"send_event_details_to": "Termindetails senden an",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -278,4 +278,4 @@
|
||||
"location_variable": "Τοποθεσία",
|
||||
"additional_notes_variable": "Πρόσθετες σημειώσεις",
|
||||
"already_have_account": "Έχετε ήδη λογαριασμό;"
|
||||
}
|
||||
}
|
||||
@@ -131,6 +131,7 @@
|
||||
"invitee_timezone": "Invitee Time Zone",
|
||||
"time_left": "Time left",
|
||||
"event_type": "Event Type",
|
||||
"duplicate_event_type":"Duplicate Event Type",
|
||||
"enter_meeting": "Enter Meeting",
|
||||
"video_call_provider": "Video call provider",
|
||||
"meeting_id": "Meeting ID",
|
||||
@@ -251,6 +252,7 @@
|
||||
"confirm_auth_change": "This will change the way you log in",
|
||||
"confirm_auth_email_change": "Changing the email address will disconnect your current authentication method to log in to Cal.com. We will ask you to verify your new email address. Moving forward, you will be logged out and use your new email address to log in instead of your current authentication method after setting your password by following the instructions that will be sent to your mail.",
|
||||
"reset_your_password": "Set your new password with the instructions sent to your email address.",
|
||||
"org_banner_instructions":"Please upload image of {{width}} width and {{height}} height.",
|
||||
"email_change": "Log back in with your new email address and password.",
|
||||
"create_your_account": "Create your account",
|
||||
"create_your_calcom_account": "Create your Cal.com account",
|
||||
@@ -704,8 +706,10 @@
|
||||
"assigned_to": "Assigned to",
|
||||
"you_must_be_logged_in_to":"You must be logged in to {{url}}",
|
||||
"start_assigning_members_above": "Start assigning members above",
|
||||
"locked_fields_admin_description": "Members will not be able to edit this",
|
||||
"locked_fields_member_description": "This option was locked by the team admin",
|
||||
"locked_fields_admin_description": "Members can not edit",
|
||||
"unlocked_fields_admin_description": "Members can edit",
|
||||
"locked_fields_member_description": "Locked by the team admin",
|
||||
"unlocked_fields_member_description": "Unlocked by team admin",
|
||||
"url": "URL",
|
||||
"hidden": "Hidden",
|
||||
"readonly": "Readonly",
|
||||
@@ -1081,6 +1085,7 @@
|
||||
"make_team_private": "Make team private",
|
||||
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
|
||||
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
|
||||
"you_cannot_see_teams_of_org":"You cannot see teams of a private organization.",
|
||||
"allow_booker_to_select_duration": "Allow booker to select duration",
|
||||
"impersonate_user_tip": "All uses of this feature is audited.",
|
||||
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
|
||||
@@ -1272,7 +1277,7 @@
|
||||
"recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan",
|
||||
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
|
||||
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
|
||||
"show_eventtype_on_profile": "Show on profile",
|
||||
"show_eventtype_on_profile": "Show on Profile",
|
||||
"embed": "Embed",
|
||||
"new_username": "New username",
|
||||
"current_username": "Current username",
|
||||
@@ -1601,7 +1606,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.",
|
||||
@@ -1615,6 +1621,7 @@
|
||||
"test_routing": "Test Routing",
|
||||
"payment_app_disabled": "An admin has disabled a payment app",
|
||||
"edit_event_type": "Edit event type",
|
||||
"only_admin_can_see_members_of_org": "This Organization is private, and only the organization's admin or owner can view its members.",
|
||||
"collective_scheduling": "Collective Scheduling",
|
||||
"make_it_easy_to_book": "Make it easy to book your team when everyone is available.",
|
||||
"find_the_best_person": "Find the best person available and cycle through your team.",
|
||||
@@ -1887,10 +1894,16 @@
|
||||
"requires_at_least_one_schedule": "You are required to have at least one schedule",
|
||||
"default_conferencing_bulk_description": "Update the locations for the selected event types",
|
||||
"locked_for_members": "Locked for members",
|
||||
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
|
||||
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
|
||||
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
|
||||
"locked_by_admin": "Locked by team admin",
|
||||
"unlocked_for_members": "Unlocked for members",
|
||||
"apps_locked_for_members_description": "Members will be able to see the active apps but will not be able to edit any app settings",
|
||||
"apps_unlocked_for_members_description": "Members will be able to see the active apps and will be able to edit any app settings",
|
||||
"apps_locked_by_team_admins_description": "You will be able to see the active apps but will not be able to edit any app settings",
|
||||
"apps_unlocked_by_team_admins_description": "You will be able to see the active apps and will be able to edit any app settings",
|
||||
"workflows_locked_for_members_description": "Members can not add their personal workflows to this event type. Members will be able to see the active team workflows but will not be able to edit any workflow settings.",
|
||||
"workflows_unlocked_for_members_description": "Members will be able to add their personal workflows to this event type. Members will be able to see the active team workflows but will not be able to edit any workflow settings.",
|
||||
"workflows_locked_by_team_admins_description": "You will be able to see the active team workflows but will not be able to edit any workflow settings or add your personal workflows to this event type.",
|
||||
"workflows_unlocked_by_team_admins_description": "You will be able to enable/disable personal workflows on this event type. You will be able to see the active team workflows but will not be able to edit any team workflow settings.",
|
||||
"locked_by_team_admin": "Locked by team admin",
|
||||
"app_not_connected": "You have not connected a {{appName}} account.",
|
||||
"connect_now": "Connect now",
|
||||
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
|
||||
@@ -2082,6 +2095,7 @@
|
||||
"organizations": "Organizations",
|
||||
"upload_cal_video_logo":"Upload Cal Video Logo",
|
||||
"update_cal_video_logo":"Update Cal Video Logo",
|
||||
"upload_banner": "Upload Banner",
|
||||
"cal_video_logo_upload_instruction":"To ensure your logo is visible against Cal video's dark background, please upload a light-colored image in PNG or SVG format to maintain transparency.",
|
||||
"org_admin_other_teams": "Other teams",
|
||||
"org_admin_other_teams_description": "Here you can see teams inside your organization that you are not part of. You can add yourself to them if needed.",
|
||||
@@ -2139,6 +2153,24 @@
|
||||
"scheduling_for_your_team":"Workflow automation",
|
||||
"scheduling_for_your_team_description":"Schedule for your team with collective and round-robin scheduling",
|
||||
"no_members_found": "No members found",
|
||||
"directory_sync" : "Directory Sync",
|
||||
"directory_name": "Directory Name",
|
||||
"directory_provider": "Directory Provider",
|
||||
"directory_scim_url": "SCIM Base URL",
|
||||
"directory_scim_token": "SCIM Bearer Token",
|
||||
"directory_scim_url_copied": "SCIM Base URL copied",
|
||||
"directory_scim_token_copied": "SCIM Bearer Token copied",
|
||||
"directory_sync_info_description": "Your Identity Provider will ask for the following information to configure SCIM. Follow the instructions to finish the setup.",
|
||||
"directory_sync_configure" : "Configure Directory Sync",
|
||||
"directory_sync_configure_description" : "Choose an identity provider to configure directory for your team.",
|
||||
"directory_sync_title": "Configure an identity provider to get started with SCIM.",
|
||||
"directory_sync_created": "Directory sync connection created.",
|
||||
"directory_sync_description": "Provision and de-provision users with your directory provider.",
|
||||
"directory_sync_deleted": "Directory sync connection deleted.",
|
||||
"directory_sync_delete_connection": "Delete Connection",
|
||||
"directory_sync_delete_title": "Delete Directory Sync Connection",
|
||||
"directory_sync_delete_description": "Are you sure you want to delete this directory sync connection?",
|
||||
"directory_sync_delete_confirmation": "This action cannot be undone. This will permanently delete the directory sync connection.",
|
||||
"event_setup_length_error":"Event Setup: The duration must be at least 1 minute.",
|
||||
"availability_schedules":"Availability Schedules",
|
||||
"unauthorized":"Unauthorized",
|
||||
@@ -2170,7 +2202,9 @@
|
||||
"overlay_my_calendar":"Overlay my calendar",
|
||||
"overlay_my_calendar_toc":"By connecting to your calendar, you accept our privacy policy and terms of use. You may revoke access at any time.",
|
||||
"view_overlay_calendar_events":"View your calendar events to prevent clashed booking.",
|
||||
"join_event_location":"Join {{eventLocationType}}",
|
||||
"locked": "Locked",
|
||||
"unlocked": "Unlocked",
|
||||
"join_event_location": "Join {{eventLocationType}}",
|
||||
"troubleshooting":"Troubleshooting",
|
||||
"calendars_were_checking_for_conflicts":"Calendars we’re checking for conflicts",
|
||||
"availabilty_schedules":"Availability schedules",
|
||||
@@ -2265,5 +2299,18 @@
|
||||
"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",
|
||||
"privacy_organization_description": "Manage privacy settings for your organization",
|
||||
"privacy": "Privacy",
|
||||
"team_will_be_under_org": "New teams will be under your organization",
|
||||
"add_group_name": "Add group name",
|
||||
"group_name": "Group Name",
|
||||
"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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1822,10 +1822,8 @@
|
||||
"requires_at_least_one_schedule": "Es necesario tener al menos un horario",
|
||||
"default_conferencing_bulk_description": "Actualizar las ubicaciones para los tipos de eventos seleccionados",
|
||||
"locked_for_members": "Bloqueado para miembros",
|
||||
"locked_apps_description": "Los miembros podrán ver las aplicaciones activas, pero no podrán editar ninguna configuración de la aplicación",
|
||||
"locked_webhooks_description": "Los miembros podrán ver los webhooks activos, pero no podrán editar ninguna configuración de webhooks",
|
||||
"locked_workflows_description": "Los miembros podrán ver los flujos de trabajo activos, pero no podrán editar ninguna configuración de flujo de trabajo",
|
||||
"locked_by_admin": "Bloqueado por el administrador del equipo",
|
||||
"apps_locked_for_members_description": "Los miembros podrán ver las aplicaciones activas, pero no podrán editar ninguna configuración de la aplicación",
|
||||
"locked_by_team_admin": "Bloqueado por el administrador del equipo",
|
||||
"app_not_connected": "No ha conectado una cuenta de {{appName}}.",
|
||||
"connect_now": "Conectar ahora",
|
||||
"managed_event_dialog_confirm_button_one": "Reemplazar y notificar a {{count}} miembro",
|
||||
@@ -2060,6 +2058,7 @@
|
||||
"workflow_automation": "Automatización del flujo de trabajo",
|
||||
"scheduling_for_your_team": "Automatización del flujo de trabajo",
|
||||
"no_members_found": "No se encontraron miembros",
|
||||
"directory_sync": "Directory Sync",
|
||||
"event_setup_length_error": "Configuración del evento: la duración debe ser de al menos 1 minuto.",
|
||||
"availability_schedules": "Horarios de disponibilidad",
|
||||
"unauthorized": "Sin autorización",
|
||||
@@ -2096,5 +2095,6 @@
|
||||
"extensive_whitelabeling": "Asistencia dedicada en materia de incorporación e ingeniería",
|
||||
"need_help": "¿Necesita ayuda?",
|
||||
"show_more": "Mostrar más",
|
||||
"privacy": "Privacidad",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -761,4 +761,4 @@
|
||||
"radio": "Irratia",
|
||||
"all_bookings_filter_label": "Erreserba guztiak",
|
||||
"timezone_variable": "Ordu-eremua"
|
||||
}
|
||||
}
|
||||
@@ -251,6 +251,7 @@
|
||||
"confirm_auth_change": "Cela changera la façon dont vous vous connectez",
|
||||
"confirm_auth_email_change": "La modification de l'adresse e-mail déconnectera votre méthode d'authentification actuelle pour vous connecter à Cal.com. Nous vous demanderons de vérifier votre nouvelle adresse e-mail. Ensuite, vous serez déconnecté et devrez utiliser votre nouvelle adresse e-mail pour vous connecter après avoir défini votre mot de passe en suivant les instructions qui seront envoyées à votre adresse e-mail.",
|
||||
"reset_your_password": "Définissez votre nouveau mot de passe avec les instructions envoyées à votre adresse e-mail.",
|
||||
"org_banner_instructions": "Veuillez télécharger une image d'une largeur de {{width}} et d'une hauteur de {{height}}.",
|
||||
"email_change": "Reconnectez-vous avec votre nouvelle adresse e-mail et votre nouveau mot de passe.",
|
||||
"create_your_account": "Créez votre compte",
|
||||
"create_your_calcom_account": "Créez votre compte Cal.com",
|
||||
@@ -808,6 +809,7 @@
|
||||
"label": "Libellé",
|
||||
"placeholder": "Texte de substitution",
|
||||
"display_add_to_calendar_organizer": "Afficher l'e-mail « Ajouter au calendrier » en tant qu'organisateur",
|
||||
"reconnect_calendar_to_use": "Veuillez noter que vous devrez peut-être vous déconnecter puis reconnecter votre compte 'Ajouter au calendrier' pour utiliser cette fonctionnalité.",
|
||||
"type": "Type",
|
||||
"edit": "Modifier",
|
||||
"add_input": "Ajouter un champ",
|
||||
@@ -1474,6 +1476,8 @@
|
||||
"copy_link_to_form": "Copier le lien vers le formulaire",
|
||||
"theme": "Apparence",
|
||||
"theme_applies_note": "Ceci s'applique uniquement à vos pages publiques de réservation.",
|
||||
"app_theme": "Thème du tableau de bord",
|
||||
"app_theme_applies_note": "Cela s'applique uniquement à votre tableau de bord connecté",
|
||||
"theme_system": "Automatique",
|
||||
"add_a_team": "Ajouter une équipe",
|
||||
"add_webhook_description": "Recevez les données de rendez-vous en temps réel lorsque quelque chose se passe sur {{appName}}.",
|
||||
@@ -1599,6 +1603,7 @@
|
||||
"attendee_email_info": "Adresse e-mail du participant",
|
||||
"kbar_search_placeholder": "Saisissez une commande ou une recherche...",
|
||||
"invalid_credential": "Oh non ! On dirait que l’autorisation a expiré ou a été révoquée. Veuillez la réinstaller.",
|
||||
"invalid_credential_action": "Réinstaller l'application",
|
||||
"reschedule_reason": "Raison de la replanification",
|
||||
"choose_common_schedule_team_event": "Choisissez un horaire commun",
|
||||
"choose_common_schedule_team_event_description": "Activez cette option si vous souhaitez utiliser des disponibilités communes entre les hôtes. Si cette option est désactivée, chaque hôte recevra les réservations en fonction de ses disponibilités par défaut.",
|
||||
@@ -1884,10 +1889,8 @@
|
||||
"requires_at_least_one_schedule": "Vous devez avoir au moins un planning",
|
||||
"default_conferencing_bulk_description": "Mettez à jour les emplacements pour les types d'événements sélectionnés.",
|
||||
"locked_for_members": "Verrouillé pour les membres",
|
||||
"locked_apps_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_webhooks_description": "Les membres pourront voir les webhooks actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_workflows_description": "Les membres pourront voir les workflows actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_by_admin": "Verrouillé par l'administrateur de l'équipe",
|
||||
"apps_locked_for_members_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_by_team_admin": "Verrouillé par l'administrateur de l'équipe",
|
||||
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
|
||||
"connect_now": "Connecter maintenant",
|
||||
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",
|
||||
@@ -2079,6 +2082,7 @@
|
||||
"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",
|
||||
"upload_banner": "Télécharger une bannière",
|
||||
"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.",
|
||||
@@ -2136,6 +2140,24 @@
|
||||
"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é",
|
||||
"directory_sync": "Synchronisation des dossiers",
|
||||
"directory_name": "Nom du répertoire",
|
||||
"directory_provider": "Fournisseur du répertoire",
|
||||
"directory_scim_url": "URL de base SCIM",
|
||||
"directory_scim_token": "Jeton du porteur SCIM",
|
||||
"directory_scim_url_copied": "URL de base SCIM copiée",
|
||||
"directory_scim_token_copied": "Le jeton SCIM Bearer a été copié",
|
||||
"directory_sync_info_description": "Votre Fournisseur d'identité vous demandera les informations suivantes pour configurer SCIM. Suivez les instructions pour terminer l'installation.",
|
||||
"directory_sync_configure": "Configurer la synchronisation du répertoire",
|
||||
"directory_sync_configure_description": "Choisissez un fournisseur d'identité pour configurer le répertoire de votre équipe.",
|
||||
"directory_sync_title": "Configurer un fournisseur d'identité pour commencer avec SCIM.",
|
||||
"directory_sync_created": "Connexion de synchronisation du répertoire créée.",
|
||||
"directory_sync_description": "Fourniture et dispense des utilisateurs avec votre fournisseur de répertoire.",
|
||||
"directory_sync_deleted": "Connexion de synchronisation du répertoire supprimé.",
|
||||
"directory_sync_delete_connection": "Supprimer la connexion",
|
||||
"directory_sync_delete_title": "Supprimer la connexion de synchronisation de répertoire",
|
||||
"directory_sync_delete_description": "Êtes-vous sûr de vouloir supprimer cette connexion de synchronisation de répertoire?",
|
||||
"directory_sync_delete_confirmation": "Cette action ne peut pas être annulée. Cela supprimera définitivement la connexion de synchronisation du répertoire.",
|
||||
"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é",
|
||||
@@ -2262,5 +2284,14 @@
|
||||
"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é",
|
||||
"privacy": "Confidentialité",
|
||||
"routers": "Routeurs",
|
||||
"primary": "Primaire",
|
||||
"make_primary": "Rendre primaire",
|
||||
"add_email": "Ajouter un e-mail",
|
||||
"add_email_description": "Ajoutez une adresse e-mail pour remplacer votre adresse principale ou pour utiliser comme adresse de courriel alternative sur vos types d'événements.",
|
||||
"confirm_email": "Confirmez votre adresse e-mail",
|
||||
"confirm_email_description": "Nous avons envoyé un e-mail à <strong>{{email}}</strong>. Cliquez sur le lien dans l'e-mail pour vérifier cette adresse.",
|
||||
"send_event_details_to": "Envoyer les détails de l'événement à",
|
||||
"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}}",
|
||||
@@ -131,6 +131,7 @@
|
||||
"invitee_timezone": "אזור הזמן של האדם שהוזמן",
|
||||
"time_left": "זמן שנותר",
|
||||
"event_type": "סוג האירוע",
|
||||
"duplicate_event_type": "שכפול סוג אירוע",
|
||||
"enter_meeting": "להצטרף לפגישה",
|
||||
"video_call_provider": "ספק שיחות וידאו",
|
||||
"meeting_id": "מזהה הפגישה",
|
||||
@@ -251,6 +252,7 @@
|
||||
"confirm_auth_change": "זה ישנה את דרך ההתחברות שלך",
|
||||
"confirm_auth_email_change": "שינוי כתובת הדוא\"ל ינתק את שיטת האימות הנוכחית שמשמשת אותך לכניסה אל Cal.com. נבקש ממך לאמת את כתובת הדוא\"ל החדשה שלך. מעכשיו, המערכת תנתק אותך מהחשבון ויהיה עליך להשתמש בכתובת הדוא\"ל החדשה שלך כדי להתחבר, במקום בשיטת האימות הנוכחית שלך לאחר שתגדיר/י סיסמה לפי ההוראות שיישלחו אליך בדוא\"ל.",
|
||||
"reset_your_password": "הגדר/י את הסיסמה החדשה שלך לפי ההוראות שנשלחו אל כתובת הדוא\"ל שלך.",
|
||||
"org_banner_instructions": "נא להעלות תמונה ברוחב {{width}} וגובה {{height}}.",
|
||||
"email_change": "התחבר/י שוב עם כתובת הדוא\"ל החדשה והסיסמה.",
|
||||
"create_your_account": "צור את החשבון שלך",
|
||||
"create_your_calcom_account": "יצירת החשבון שלך ב־Cal.com",
|
||||
@@ -705,7 +707,9 @@
|
||||
"you_must_be_logged_in_to": "חובה להיכנס אל {{url}}",
|
||||
"start_assigning_members_above": "התחל/י להקצות חברים למעלה",
|
||||
"locked_fields_admin_description": "חברים לא יוכלו לערוך את זה",
|
||||
"unlocked_fields_admin_description": "חברים יכולים לערוך",
|
||||
"locked_fields_member_description": "מנהל הצוות נעל את האפשרות הזו",
|
||||
"unlocked_fields_member_description": "שוחרר על ידי הנהלת הצוות",
|
||||
"url": "כתובת URL",
|
||||
"hidden": "מוסתר",
|
||||
"readonly": "לקריאה בלבד",
|
||||
@@ -1602,6 +1606,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": "יש להפעיל אפשרות זו אם רוצים להשתמש בלוח זמנים משותף בין המארחים. אם האפשרות הזו מבוטלת, כל מארח יוזמן לפי לוח הזמנים המשמש כברירת המחדל שלו.",
|
||||
@@ -1887,10 +1892,12 @@
|
||||
"requires_at_least_one_schedule": "אתה חייב שיהיה לך לפחות לוח זמנים אחד",
|
||||
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו",
|
||||
"locked_for_members": "נעול לחברים",
|
||||
"locked_apps_description": "החברים יוכלו לראות את האפליקציות הפעילות, אבל לא יוכלו לערוך את הגדרות האפליקציה",
|
||||
"locked_webhooks_description": "החברים יוכלו לראות את רכיבי ה-Webhook הפעילים, אבל לא יוכלו לערוך הגדרות של רכיבי Webhook",
|
||||
"locked_workflows_description": "החברים יוכלו לראות את תהליכי העבודה הפעילים, אבל לא יוכלו לערוך הגדרות של תהליכי עבודה",
|
||||
"locked_by_admin": "ננעל על ידי מנהל/ת הצוות",
|
||||
"unlocked_for_members": "שוחרר לטובת החברים",
|
||||
"apps_locked_for_members_description": "החברים יוכלו לראות את האפליקציות הפעילות, אבל לא יוכלו לערוך את הגדרות האפליקציה",
|
||||
"apps_unlocked_for_members_description": "החברים יוכלו לראות את היישומים הפעילים ויוכלו לערוך את כל הגדרות היישומים",
|
||||
"apps_locked_by_team_admins_description": "יתאפשר לך לראות את היישומים הפעילים, אבל לא לערוך הגדרות יישומים כלשהן",
|
||||
"apps_unlocked_by_team_admins_description": "יתאפשר לך לראות את היישומים הפעילים ולערוך את כל הגדרות היישומים",
|
||||
"locked_by_team_admin": "ננעל על ידי מנהל/ת הצוות",
|
||||
"app_not_connected": "לא קישרת חשבון {{appName}}.",
|
||||
"connect_now": "להתחבר עכשיו",
|
||||
"managed_event_dialog_confirm_button_one": "החלפה ועדכון של חבר/ה {{count}}",
|
||||
@@ -2082,6 +2089,7 @@
|
||||
"organizations": "ארגונים",
|
||||
"upload_cal_video_logo": "העלאת סרטון לוגו ל־Cal",
|
||||
"update_cal_video_logo": "עדכון סרטון לוגו ל־Cal",
|
||||
"upload_banner": "העלאת כרזה",
|
||||
"cal_video_logo_upload_instruction": "כדי לוודא שהלוגו שלך גלוי כנגד הרקע הכהה של הסרטון של Cal, נא להעלות תמונה בצבעים בהירים מהסוגים PNG או SVG כדי שהחלקים המתאימים יישארו שקופים.",
|
||||
"org_admin_other_teams": "צוותים אחרים",
|
||||
"org_admin_other_teams_description": "כאן תוכל/י לראות צוותים בארגון שאינך שייך/ת אליהם. יש לך אפשרות להוסיף את עצמך, במקרה הצורך.",
|
||||
@@ -2139,6 +2147,10 @@
|
||||
"scheduling_for_your_team": "אוטומצית תהליך עבודה",
|
||||
"scheduling_for_your_team_description": "עריכת תזמון לצוות שלך באופן שיתופי ולפי התור",
|
||||
"no_members_found": "לא נמצא אף חבר",
|
||||
"directory_sync": "סנכרון ספריות",
|
||||
"directory_name": "שם הספרייה",
|
||||
"directory_provider": "ספק ספרייה",
|
||||
"directory_sync_configure": "הגדרת סנכרון ספרייה",
|
||||
"event_setup_length_error": "הגדרת אירוע: משך הזמן חייב להיות לפחות דקה אחת.",
|
||||
"availability_schedules": "לוחות זמנים לזמינוּת",
|
||||
"unauthorized": "אין הרשאה",
|
||||
@@ -2170,6 +2182,8 @@
|
||||
"overlay_my_calendar": "הצג את לוח השנה שלי בשכבת-על",
|
||||
"overlay_my_calendar_toc": "על ידי חיבור אל לוח השנה שלך, את/ה מקבל/ת את מדיניות הפרטיות ואת תנאי השימוש שלנו. אפשר לשלול את הגישה בכל שלב.",
|
||||
"view_overlay_calendar_events": "ראה/י את האירועים שלך בלוח השנה כדי למנוע התנגשות בהזמנות.",
|
||||
"locked": "נעול",
|
||||
"unlocked": "משוחרר",
|
||||
"join_event_location": "הצטרפות אל {{eventLocationType}}",
|
||||
"troubleshooting": "פתרון בעיות",
|
||||
"calendars_were_checking_for_conflicts": "לוחות השנה לא בודקים סתירות",
|
||||
@@ -2265,5 +2279,17 @@
|
||||
"field_identifiers_as_variables": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית",
|
||||
"field_identifiers_as_variables_with_example": "להשתמש במזהי השדות כמשתנים להפניות האירועים בהתאמה אישית (למשל: {{variable}})",
|
||||
"account_already_linked": "החשבון כבר מקושר",
|
||||
"privacy": "פרטיות",
|
||||
"team_will_be_under_org": "צוותים חדשים יהיו תחת הארגון שלך",
|
||||
"add_group_name": "הוספת שם קבוצה",
|
||||
"group_name": "שם קבוצה",
|
||||
"routers": "נתבים",
|
||||
"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 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -335,4 +335,4 @@
|
||||
"user_dynamic_booking_disabled": "Neki od korisnika u grupi trenutno su onemogućili dinamičke grupne rezervacije",
|
||||
"full_name": "Puno ime",
|
||||
"already_have_account": "Već imate račun?"
|
||||
}
|
||||
}
|
||||
@@ -7,21 +7,77 @@
|
||||
"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?",
|
||||
"new_event_scheduled_recurring": "Egy új ismétlődő időpont le lett foglalva.",
|
||||
"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",
|
||||
@@ -55,6 +111,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",
|
||||
@@ -69,6 +126,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",
|
||||
@@ -81,9 +140,30 @@
|
||||
"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",
|
||||
"meeting_is_scheduled_recurring": "Az ismétlődő időpontod le lett foglalva",
|
||||
"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",
|
||||
@@ -91,6 +171,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...",
|
||||
@@ -123,13 +210,24 @@
|
||||
"friday": "Péntek",
|
||||
"saturday": "Szombat",
|
||||
"sunday": "Vasárnap",
|
||||
"additional_guests": "További résztvevők",
|
||||
"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",
|
||||
"organizer_phone_number": "Szervező telefonszáma",
|
||||
"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",
|
||||
@@ -138,8 +236,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",
|
||||
@@ -147,7 +248,9 @@
|
||||
"share": "Megosztás",
|
||||
"title": "Cím",
|
||||
"description": "Leírás",
|
||||
"apps_status": "Alkalmazások állapota",
|
||||
"duration": "Hossz",
|
||||
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
|
||||
"minutes": "Perc",
|
||||
"url": "URL",
|
||||
"hidden": "Rejtett",
|
||||
@@ -155,15 +258,21 @@
|
||||
"your_email": "Email címed",
|
||||
"language": "Nyelv",
|
||||
"timezone": "Időzóna",
|
||||
"every_for_freq": "Minden {{freq}}",
|
||||
"repeats_every": "Ismétlődik minden",
|
||||
"occurrence_other": "alkalomig",
|
||||
"weekly_one": "hét",
|
||||
"weekly_other": "hét",
|
||||
"monthly_one": "hónap",
|
||||
"monthly_other": "hónap",
|
||||
"yearly_one": "év",
|
||||
"yearly_other": "év",
|
||||
"plus_more": "{{count}} további alkalom",
|
||||
"name": "Név",
|
||||
"logo": "Logó",
|
||||
"error": "Hiba",
|
||||
"meeting_with_user": "{Event type title} {Organiser} és {Scheduler} közt",
|
||||
"display_add_to_calendar_organizer": "Jelenítse meg a 'Hozzáadás naptárhoz' emailt mint szervező",
|
||||
"type": "Típus",
|
||||
"edit": "Szerkesztés",
|
||||
"starting": "Kezdés",
|
||||
@@ -189,13 +298,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",
|
||||
@@ -204,12 +326,74 @@
|
||||
"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}}",
|
||||
"organizer_name_variable": "Szervező név",
|
||||
"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"
|
||||
}
|
||||
"additional_notes_info": "A találkozó egyéb megjegyzései",
|
||||
"organizer_name_info": "Szervező neve",
|
||||
"require_additional_notes": "Kötelező kitölteni az egyéb megjegyzéseket",
|
||||
"require_additional_notes_description": "Kötelező kitölteni az egyéb megjegyzéeseket a foglaláshoz",
|
||||
"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",
|
||||
"organizer_timezone": "Szervező időzónája",
|
||||
"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}}",
|
||||
"google_new_spam_policy": "Google új levélszemét szabályzata megakadályozhatja, hogy megérkezzenek az email és naptár értesítések erről a foglalásról.",
|
||||
"resolve": "Megoldás",
|
||||
"what_is_this_meeting_about": "Miről szól ez a találkozó?",
|
||||
"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.",
|
||||
"need_help": "Segítségre van szükséged?",
|
||||
"booking_redirect_email_subject": "Foglalás átirányítás értesítő",
|
||||
"team_will_be_under_org": "Új csapatok a szervezeted alá fognak tartozni",
|
||||
"add_group_name": "Adj hozzá csapatnevet",
|
||||
"group_name": "Csapat név",
|
||||
"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"
|
||||
}
|
||||
@@ -127,4 +127,4 @@
|
||||
"incorrect_2fa_code": "Kode dua faktor salah.",
|
||||
"no_account_exists": "Tidak ada akun dengan email tersebut.",
|
||||
"need_help": "Butuh bantuan?"
|
||||
}
|
||||
}
|
||||
@@ -808,6 +808,7 @@
|
||||
"label": "Etichetta",
|
||||
"placeholder": "Segnaposto",
|
||||
"display_add_to_calendar_organizer": "Visualizza l'email \"Aggiungi al calendario\" come organizzatore",
|
||||
"reconnect_calendar_to_use": "Nota che potrebbe essere necessario disconnettere e poi riconnettere l'account \"Aggiungi al calendario\" per utilizzare questa funzione.",
|
||||
"type": "Tipo",
|
||||
"edit": "Modifica",
|
||||
"add_input": "Aggiungi un input",
|
||||
@@ -1474,6 +1475,8 @@
|
||||
"copy_link_to_form": "Copia il link del modulo",
|
||||
"theme": "Tema",
|
||||
"theme_applies_note": "Si applica solo alle pagine di prenotazione pubbliche",
|
||||
"app_theme": "Tema Pannello di controllo",
|
||||
"app_theme_applies_note": "Questo riguarda solo il pannello di controllo per cui hai effettuato l'accesso",
|
||||
"theme_system": "Predefinito di sistema",
|
||||
"add_a_team": "Aggiungi un team",
|
||||
"add_webhook_description": "Ricevi in tempo reale le informazioni sulle riunioni quando succede qualcosa su {{appName}}",
|
||||
@@ -1599,6 +1602,7 @@
|
||||
"attendee_email_info": "E-mail della persona che prenota",
|
||||
"kbar_search_placeholder": "Digita un comando o esegui una ricerca...",
|
||||
"invalid_credential": "Errore! Sembra che l'autorizzazione sia scaduta o che sia stata revocata. Prova a reinstallare.",
|
||||
"invalid_credential_action": "Reinstalla app",
|
||||
"reschedule_reason": "Motivo della riprogrammazione",
|
||||
"choose_common_schedule_team_event": "Scegli un programma comune",
|
||||
"choose_common_schedule_team_event_description": "Abilita questa opzione se desideri utilizzare un programma comune per tutti gli organizzatori. Quando l'opzione è disabilitata, ciascun organizzatore verrà prenotato in base al proprio programma predefinito.",
|
||||
@@ -1884,10 +1888,8 @@
|
||||
"requires_at_least_one_schedule": "È necessario avere almeno un programma",
|
||||
"default_conferencing_bulk_description": "Aggiorna le posizioni per i tipi di eventi selezionati",
|
||||
"locked_for_members": "Bloccato per i membri",
|
||||
"locked_apps_description": "I membri potranno vedere le app attive, ma non potranno modificare le impostazioni delle app",
|
||||
"locked_webhooks_description": "I membri potranno vedere i webhook attivi, ma non potranno modificare le impostazioni dei webhook",
|
||||
"locked_workflows_description": "I membri potranno vedere i flussi di lavoro attivi, ma non potranno modificare le impostazioni dei flussi di lavoro",
|
||||
"locked_by_admin": "Bloccato dall'amministratore del team",
|
||||
"apps_locked_for_members_description": "I membri potranno vedere le app attive, ma non potranno modificare le impostazioni delle app",
|
||||
"locked_by_team_admin": "Bloccato dall'amministratore del team",
|
||||
"app_not_connected": "Non è stato connesso un account di {{appName}}.",
|
||||
"connect_now": "Connetti ora",
|
||||
"managed_event_dialog_confirm_button_one": "Sostituisci e invia notifica a {{count}} membro",
|
||||
@@ -2136,6 +2138,7 @@
|
||||
"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",
|
||||
"directory_sync": "Directory Sync",
|
||||
"event_setup_length_error": "Impostazione evento: la durata deve essere di almeno 1 minuto.",
|
||||
"availability_schedules": "Calendario disponibilità",
|
||||
"unauthorized": "Non autorizzato",
|
||||
@@ -2262,5 +2265,13 @@
|
||||
"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",
|
||||
"privacy": "Privacy",
|
||||
"primary": "Principale",
|
||||
"make_primary": "Rendi principale",
|
||||
"add_email": "Aggiungi Email",
|
||||
"add_email_description": "Aggiungi un indirizzo email con cui sostituire quello principale o da utilizzare come email alternativa per i tipi di evento.",
|
||||
"confirm_email": "Conferma la tua email",
|
||||
"confirm_email_description": "Abbiamo inviato un'email a <strong>{{email}}</strong>. Clicca sul link nell'email per verificare questo indirizzo.",
|
||||
"send_event_details_to": "Invia dettagli evento a",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Aggiungi le tue nuove stringhe qui sopra ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "少なくとも 1 件のスケジュールが必要です",
|
||||
"default_conferencing_bulk_description": "選択したイベントの種類の場所を更新します",
|
||||
"locked_for_members": "メンバーに対してロックされています",
|
||||
"locked_apps_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません",
|
||||
"locked_webhooks_description": "メンバーは有効なウェブフックを確認できますが、ウェブフックの設定を編集することはできません",
|
||||
"locked_workflows_description": "メンバーは有効なワークフローを確認できますが、ワークフローの設定を編集することはできません",
|
||||
"locked_by_admin": "チームの管理者によりロックされています",
|
||||
"apps_locked_for_members_description": "メンバーは有効なアプリを確認できますが、アプリの設定を編集することはできません",
|
||||
"locked_by_team_admin": "チームの管理者によりロックされています",
|
||||
"app_not_connected": "{{appName}} のアカウントに接続していません。",
|
||||
"connect_now": "今すぐ接続",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} 人のメンバーを置き換えて通知する",
|
||||
@@ -2061,6 +2059,7 @@
|
||||
"workflow_automation": "ワークフローの自動化",
|
||||
"scheduling_for_your_team": "ワークフローの自動化",
|
||||
"no_members_found": "メンバーが見つかりません",
|
||||
"directory_sync": "Directory Sync",
|
||||
"event_setup_length_error": "イベント設定:時間は 1 分以上でなくてはいけません。",
|
||||
"availability_schedules": "空き状況一覧",
|
||||
"unauthorized": "権限がありません",
|
||||
@@ -2097,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "専用のオンボーディングサポートとエンジニアリングサポート",
|
||||
"need_help": "サポートが必要ですか?",
|
||||
"show_more": "さらに表示",
|
||||
"privacy": "プライバシー",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ この上に新しい文字列を追加してください ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -253,4 +253,4 @@
|
||||
"user": "អ្នកប្រើប្រាស់",
|
||||
"general_description": "គ្រប់គ្រងការកំណត់សម្រាប់ភាសា និងល្វែងម៉ោងរបស់អ្នក។",
|
||||
"timezone_variable": "ល្វែងម៉ោង"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "적어도 하나 이상의 일정이 필요합니다",
|
||||
"default_conferencing_bulk_description": "선택한 이벤트 타입의 위치 업데이트",
|
||||
"locked_for_members": "회원에 대해 잠김",
|
||||
"locked_apps_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다",
|
||||
"locked_webhooks_description": "회원은 활성 웹훅을 볼 수 있지만 웹훅 설정을 편집할 수는 없습니다",
|
||||
"locked_workflows_description": "회원은 활성 워크플로를 볼 수 있지만 워크플로 설정을 편집할 수는 없습니다",
|
||||
"locked_by_admin": "팀 관리자에 의해 잠김",
|
||||
"apps_locked_for_members_description": "회원은 활성 앱을 볼 수 있지만 앱 설정을 편집할 수는 없습니다",
|
||||
"locked_by_team_admin": "팀 관리자에 의해 잠김",
|
||||
"app_not_connected": "{{appName}} 계정을 연결하지 않았습니다.",
|
||||
"connect_now": "지금 연결",
|
||||
"managed_event_dialog_confirm_button_one": "회원 {{count}}명 바꾸기 및 알림",
|
||||
@@ -2061,6 +2059,7 @@
|
||||
"workflow_automation": "워크플로 자동화",
|
||||
"scheduling_for_your_team": "워크플로 자동화",
|
||||
"no_members_found": "구성원 없음",
|
||||
"directory_sync": "디렉토리 동기화",
|
||||
"event_setup_length_error": "이벤트 설정: 지속 시간은 1분 이상이어야 합니다.",
|
||||
"availability_schedules": "사용 가능한 일정",
|
||||
"unauthorized": "미승인",
|
||||
@@ -2097,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "전담 온보딩 및 엔지니어링 지원",
|
||||
"need_help": "도움이 필요하세요?",
|
||||
"show_more": "더 보기",
|
||||
"privacy": "개인정보보호",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 여기에 새 문자열을 추가하세요 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -126,4 +126,4 @@
|
||||
"confirm_password": "Apstiprināt paroli",
|
||||
"confirm_auth_change": "Šis mainīs veidu, kā jūs autorizējaties",
|
||||
"already_have_account": "Vai jums jau ir konts?"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "U moet ten minste één planning hebben",
|
||||
"default_conferencing_bulk_description": "Werk de locaties voor de geselecteerde gebeurtenistypen bij",
|
||||
"locked_for_members": "Vergrendeld voor leden",
|
||||
"locked_apps_description": "Leden kunnen de actieve apps zien, maar kunnen geen appinstellingen bewerken",
|
||||
"locked_webhooks_description": "Leden kunnen de actieve webhooks zien, maar kunnen geen webhookinstellingen bewerken",
|
||||
"locked_workflows_description": "Leden kunnen de actieve werkstromen zien, maar kunnen geen werkstroominstellingen bewerken",
|
||||
"locked_by_admin": "Vergrendeld door teambeheerder",
|
||||
"apps_locked_for_members_description": "Leden kunnen de actieve apps zien, maar kunnen geen appinstellingen bewerken",
|
||||
"locked_by_team_admin": "Vergrendeld door teambeheerder",
|
||||
"app_not_connected": "U hebt geen {{appName}}-account gekoppeld.",
|
||||
"connect_now": "Nu koppelen",
|
||||
"managed_event_dialog_confirm_button_one": "{{count}} lid vervangen en informeren",
|
||||
@@ -2061,6 +2059,7 @@
|
||||
"workflow_automation": "Automatisering van de werkstroom",
|
||||
"scheduling_for_your_team": "Automatisering van de werkstroom",
|
||||
"no_members_found": "Geen leden gevonden",
|
||||
"directory_sync": "Mapsynchronisatie",
|
||||
"event_setup_length_error": "Gebeurtenisconfiguratie: de duur moet minimaal 1 minuut zijn.",
|
||||
"availability_schedules": "Beschikbaarheidsschema's",
|
||||
"unauthorized": "Ongeautoriseerd",
|
||||
@@ -2097,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Speciale onboarding en technische ondersteuning",
|
||||
"need_help": "Hulp nodig?",
|
||||
"show_more": "Meer weergeven",
|
||||
"privacy": "Privacy",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Voeg uw nieuwe strings hierboven toe ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1389,4 +1389,4 @@
|
||||
"booking_confirmation_failed": "Booking-bekreftelse feilet",
|
||||
"timezone_variable": "Tidssone",
|
||||
"need_help": "Trenger du hjelp?"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "Musisz mieć co najmniej jeden harmonogram",
|
||||
"default_conferencing_bulk_description": "Aktualizuj lokalizacje wybranych typów wydarzeń",
|
||||
"locked_for_members": "Zablokowany dla członków",
|
||||
"locked_apps_description": "Członkowie będą mogli sprawdzić aktywne aplikacje, ale nie będą mogli edytować ich ustawień",
|
||||
"locked_webhooks_description": "Członkowie będą mogli sprawdzić aktywne webhooki, ale nie będą mogli edytować ich ustawień",
|
||||
"locked_workflows_description": "Członkowie będą mogli sprawdzić aktywne przepływy pracy, ale nie będą mogli edytować ich ustawień",
|
||||
"locked_by_admin": "Zablokowane przez administratora zespołu",
|
||||
"apps_locked_for_members_description": "Członkowie będą mogli sprawdzić aktywne aplikacje, ale nie będą mogli edytować ich ustawień",
|
||||
"locked_by_team_admin": "Zablokowane przez administratora zespołu",
|
||||
"app_not_connected": "Nie podłączono konta {{appName}}.",
|
||||
"connect_now": "Podłącz teraz",
|
||||
"managed_event_dialog_confirm_button_one": "Zastąp i poinformuj {{count}} członka",
|
||||
@@ -2061,6 +2059,7 @@
|
||||
"workflow_automation": "Automatyzacja przepływu pracy",
|
||||
"scheduling_for_your_team": "Automatyzacja przepływu pracy",
|
||||
"no_members_found": "Nie znaleziono członków",
|
||||
"directory_sync": "Synchronizacja katalogu",
|
||||
"event_setup_length_error": "Konfiguracja wydarzenia: czas trwania musi wynosić co najmniej minutę.",
|
||||
"availability_schedules": "Harmonogramy dostępności",
|
||||
"unauthorized": "Brak autoryzacji",
|
||||
@@ -2097,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Dedykowane wsparcie w zakresie wdrożenia i obsługi technicznej",
|
||||
"need_help": "Potrzebujesz pomocy?",
|
||||
"show_more": "Wyświetl więcej",
|
||||
"privacy": "Prywatność",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Dodaj nowe ciągi powyżej ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1858,10 +1858,8 @@
|
||||
"requires_at_least_one_schedule": "Você precisa ter pelo menos uma agenda",
|
||||
"default_conferencing_bulk_description": "Atualize as localizações para os tipos de evento selecionados",
|
||||
"locked_for_members": "Bloqueado para membros",
|
||||
"locked_apps_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
|
||||
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar as configurações",
|
||||
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar as configurações",
|
||||
"locked_by_admin": "Bloqueado pelo administrador da equipe",
|
||||
"apps_locked_for_members_description": "Os membros poderão ver os aplicativos ativos, mas não poderão editar as configurações",
|
||||
"locked_by_team_admin": "Bloqueado pelo administrador da equipe",
|
||||
"app_not_connected": "Você não conectou uma conta do {{appName}}.",
|
||||
"connect_now": "Conectar agora",
|
||||
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
||||
@@ -2096,6 +2094,7 @@
|
||||
"workflow_automation": "Automação de fluxo de trabalho",
|
||||
"scheduling_for_your_team": "Automação de fluxo de trabalho",
|
||||
"no_members_found": "Nenhum membro encontrado",
|
||||
"directory_sync": "Sincronização de diretório",
|
||||
"event_setup_length_error": "Configuração do evento: deve ter pelo menos 1 minuto de duração.",
|
||||
"availability_schedules": "Agendamentos de disponibilidade",
|
||||
"unauthorized": "Não autorizado",
|
||||
@@ -2132,5 +2131,6 @@
|
||||
"extensive_whitelabeling": "Marca própria abrangente",
|
||||
"need_help": "Precisa de ajuda?",
|
||||
"show_more": "Mostrar mais",
|
||||
"privacy": "Privacidade",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adicione suas novas strings aqui em cima ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -808,6 +808,7 @@
|
||||
"label": "Etiqueta",
|
||||
"placeholder": "Preenchimento",
|
||||
"display_add_to_calendar_organizer": "Mostrar o e-mail 'Adicionar ao calendário' como organizador",
|
||||
"reconnect_calendar_to_use": "Note que poderá ser necessário desligar e voltar a ligar a sua conta 'Adicionar ao Calendário' para utilizar esta funcionalidade.",
|
||||
"type": "Tipo",
|
||||
"edit": "Editar",
|
||||
"add_input": "Adicionar um Campo",
|
||||
@@ -1474,6 +1475,8 @@
|
||||
"copy_link_to_form": "Copiar ligação para o formulário",
|
||||
"theme": "Tema",
|
||||
"theme_applies_note": "Isto só se aplica às suas páginas de reservas públicas",
|
||||
"app_theme": "Tema do Painel",
|
||||
"app_theme_applies_note": "Isto aplica-se apenas ao seu painel de administração quando tem a sessão iniciada",
|
||||
"theme_system": "Predefinição do sistema",
|
||||
"add_a_team": "Adicionar uma equipa",
|
||||
"add_webhook_description": "Receba os dados da reunião em tempo real quando algo acontecer em {{appName}}",
|
||||
@@ -1599,6 +1602,7 @@
|
||||
"attendee_email_info": "O e-mail do responsável pela reserva",
|
||||
"kbar_search_placeholder": "Digite um comando ou pesquise...",
|
||||
"invalid_credential": "Oh não! Parece que a permissão expirou ou foi revogada. Por favor, reinstale novamente.",
|
||||
"invalid_credential_action": "Reinstalar aplicação",
|
||||
"reschedule_reason": "Motivo do reagendamento",
|
||||
"choose_common_schedule_team_event": "Escolha uma agenda comum",
|
||||
"choose_common_schedule_team_event_description": "Ative esta opção se quiser utilizar uma agenda comum entre anfitriões. Quando desativada, cada anfitrião será reservado com base na respetiva agenda pré-definida.",
|
||||
@@ -1884,10 +1888,8 @@
|
||||
"requires_at_least_one_schedule": "É necessário que tenha pelo menos uma agenda",
|
||||
"default_conferencing_bulk_description": "Atualizar os locais para os tipos de evento selecionados",
|
||||
"locked_for_members": "Bloqueado para os membros",
|
||||
"locked_apps_description": "Os membros poderão ver as aplicações ativas, mas não poderão editar quaisquer configurações das aplicações",
|
||||
"locked_webhooks_description": "Os membros poderão ver os webhooks ativos, mas não poderão editar quaisquer configurações dos webhooks",
|
||||
"locked_workflows_description": "Os membros poderão ver os fluxos de trabalho ativos, mas não poderão editar quaisquer configurações dos fluxos de trabalho",
|
||||
"locked_by_admin": "Bloqueado pelo administrador da equipa",
|
||||
"apps_locked_for_members_description": "Os membros poderão ver as aplicações ativas, mas não poderão editar quaisquer configurações das aplicações",
|
||||
"locked_by_team_admin": "Bloqueado pelo administrador da equipa",
|
||||
"app_not_connected": "Não associou uma conta {{appName}}.",
|
||||
"connect_now": "Associar agora",
|
||||
"managed_event_dialog_confirm_button_one": "Substituir e notificar {{count}} membro",
|
||||
@@ -2136,6 +2138,7 @@
|
||||
"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",
|
||||
"directory_sync": "Sincronização do diretório",
|
||||
"event_setup_length_error": "Configuração do Evento: a duração deve ser de, pelo menos, 1 minuto.",
|
||||
"availability_schedules": "Horários de Disponibilidade",
|
||||
"unauthorized": "Não autorizado",
|
||||
@@ -2262,5 +2265,13 @@
|
||||
"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",
|
||||
"privacy": "Privacidade",
|
||||
"primary": "Primário",
|
||||
"make_primary": "Tornar primário",
|
||||
"add_email": "Adicionar e-mail",
|
||||
"add_email_description": "Adicione um endereço de e-mail para substituir o seu primário ou para utilizar como um e-mail alternativo nos seus tipos de evento.",
|
||||
"confirm_email": "Confirme o seu e-mail",
|
||||
"confirm_email_description": "Enviámos um e-mail para <strong>{{email}}</strong>. Clique na ligação nesse e-mail para verificar este endereço.",
|
||||
"send_event_details_to": "Enviar detalhes do evento para",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1823,10 +1823,8 @@
|
||||
"requires_at_least_one_schedule": "Este necesar să aveți cel puțin un program",
|
||||
"default_conferencing_bulk_description": "Actualizați locațiile pentru tipurile de evenimente selectate",
|
||||
"locked_for_members": "Blocat pentru membri",
|
||||
"locked_apps_description": "Membrii vor putea vedea aplicațiile active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_webhooks_description": "Membrii vor putea vedea webhook-urile active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_workflows_description": "Membrii vor putea vedea fluxurile de lucru active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_by_admin": "Blocat de administratorul echipei",
|
||||
"apps_locked_for_members_description": "Membrii vor putea vedea aplicațiile active, însă nu le vor putea modifica deloc setările",
|
||||
"locked_by_team_admin": "Blocat de administratorul echipei",
|
||||
"app_not_connected": "Nu ați conectat un cont {{appName}}.",
|
||||
"connect_now": "Conectați-l acum",
|
||||
"managed_event_dialog_confirm_button_one": "Înlocuiți și anunțați {{count}} membru",
|
||||
@@ -2061,6 +2059,7 @@
|
||||
"workflow_automation": "Automatizarea fluxului de lucru",
|
||||
"scheduling_for_your_team": "Automatizarea fluxului de lucru",
|
||||
"no_members_found": "Nu s-a găsit niciun membru",
|
||||
"directory_sync": "Sincronizare director",
|
||||
"event_setup_length_error": "Configurare eveniment: Durata trebuie să fie de cel puțin 1 minut.",
|
||||
"availability_schedules": "Programe de disponibilitate",
|
||||
"unauthorized": "Neautorizat",
|
||||
@@ -2097,5 +2096,6 @@
|
||||
"extensive_whitelabeling": "Asistență dedicată pentru integrare și inginerie",
|
||||
"need_help": "Aveți nevoie de ajutor?",
|
||||
"show_more": "Afișare mai mult",
|
||||
"privacy": "Confidențialitate",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Adăugați stringurile noi deasupra acestui rând ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
@@ -1824,10 +1824,8 @@
|
||||
"requires_at_least_one_schedule": "У вас должно быть по крайней мере одно расписание",
|
||||
"default_conferencing_bulk_description": "Обновить местоположение для выбранных типов событий",
|
||||
"locked_for_members": "Заблокировано для участников",
|
||||
"locked_apps_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки",
|
||||
"locked_webhooks_description": "Участники смогут видеть активные вебхуки, но не смогут редактировать их настройки",
|
||||
"locked_workflows_description": "Участники смогут видеть активные рабочие процессы, но не смогут редактировать их настройки",
|
||||
"locked_by_admin": "Заблокировано администратором команды",
|
||||
"apps_locked_for_members_description": "Участники смогут видеть активные приложения, но не смогут редактировать их настройки",
|
||||
"locked_by_team_admin": "Заблокировано администратором команды",
|
||||
"app_not_connected": "Вы не подключили аккаунт {{appName}}.",
|
||||
"connect_now": "Подключить",
|
||||
"managed_event_dialog_confirm_button_one": "Заменить и уведомить {{count}} участника",
|
||||
@@ -2062,6 +2060,7 @@
|
||||
"workflow_automation": "Автоматизация при помощи рабочих процессов",
|
||||
"scheduling_for_your_team": "Автоматизация при помощи рабочих процессов",
|
||||
"no_members_found": "Участники не найдены",
|
||||
"directory_sync": "Синхронизация каталогов",
|
||||
"event_setup_length_error": "Настройка события: продолжительность должна быть не менее 1 минуты.",
|
||||
"availability_schedules": "График с информацией о доступности",
|
||||
"unauthorized": "Неавторизован(а)",
|
||||
@@ -2098,5 +2097,6 @@
|
||||
"extensive_whitelabeling": "Индивидуальная техподдержка и помощь при онбординге",
|
||||
"need_help": "Нужна помощь?",
|
||||
"show_more": "Показать больше",
|
||||
"privacy": "Конфиденциальность",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Добавьте строки выше ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user