Compare commits

...
49 Commits
Author SHA1 Message Date
Omar López bf9c881d16 v3.2.8
release.yml / release (push) Canceled after 0s
2023-09-05 13:41:18 -07:00
alannnc 6dd0f9ce29 fix: selected organizer when creating event (#11157) 2023-09-05 13:38:59 -07:00
d34db7aef6 fix: Fix wrong footer Button in some languages (#10873)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Shivam Kalra <shivamkalra98@gmail.com>
2023-09-05 18:44:46 +00:00
Keith WilliamsandAlan b4b90f2d27 perf: Removed unused queries for user event types (#10568)
* perf: Removed unused queries for user event types

* Added empty children array

* Refactor to clean up

* Fixing type issue

* Removed extra call to get raw event types

* Temporarily putting children back

* Removing include from children

* Added check for children

* Put children back for testing

---------

Co-authored-by: Alan <alannnc@gmail.com>
2023-09-05 11:35:02 -07:00
Peer Richelsen 3f0380617a chore: removed gravatar (#11153) 2023-09-05 19:56:02 +02:00
Udit Takkar 7143923ea4 fix: rescheduling in require confirmation (#11152)
## What does this PR do?

Fixes https://github.com/calcom/cal.com/issues/11075

BEFORE:

Meeting has been rescheduled email and calendar event after submitting booking without confirming the meeting (require confirmation event type).

<img width="1086" alt="Screenshot 2023-09-05 at 7 45 44 PM" src="https://github.com/calcom/cal.com/assets/53316345/574c8d2f-3698-44ea-9466-aef162e09dd5">

AFTER:-

event scheduled email and calendar event created only after "awaiting approval" meeting has been confirmed.

<img width="1086" alt="Screenshot 2023-09-05 at 7 47 22 PM" src="https://github.com/calcom/cal.com/assets/53316345/af19ccea-d418-4a40-a9c6-88782522df53">


## Type of change

<!-- Please delete bullets that are not relevant. -->

- [x] Bug fix (non-breaking change which fixes an issue)

## How should this be tested?

1) Create a requires confirmation event type
2) open incognito tab or another browser and book a meeting (Attendee)
3) Confirm the booking (Organizer) and attendee would get meeting schedule email
4) Get the reschedule link from attendee's email and reschedule the booking

Both attendee and organiser shouldn't get booking email or calendar event  before organiser has accepted the meeting
2023-09-05 10:52:47 -07:00
DexterStorey dfccf4604f fix: better timezones and error handling in ai tools (#11085)
* current time in user timezone, let ai handle 401

* update agent.ts

* simple onboarding for non cal.com users

* reverting this line - unecessary

* v1.0.1
2023-09-05 13:56:26 +01:00
VijayandHariom Balhara c12abd75a8 fix: #10973 back button not working as expected in embed (#11073)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2023-09-05 15:40:09 +05:30
Crowdin Bot 2e12f09f05 New Crowdin translations by Github Action 2023-09-04 23:12:36 +00:00
alannnc 4258da1df5 remove double call on useSchedule() (#11141) 2023-09-04 23:10:19 +00:00
Hariom Balhara 41ef354c7b fix: FORM_SUBMITTED webhook payload change and support for Team Webhooks with it (#10986) 2023-09-04 17:04:57 -04:00
alannncandzomars 8891641953 hotfix: missing gCal calendarId (#11139)
Co-authored-by: zomars <zomars@me.com>
2023-09-04 20:25:39 +00:00
Udit Takkar 0150041496 fix: improve UX of team invite by copy link (#11009) 2023-09-04 12:43:37 -07:00
Carina WollendorferandCarinaWolli 13f9e97015 fix: variables for custom webhook payload (#11135)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-04 11:01:12 -07:00
Peer Richelsen 3c5901edb8 chore: cal.ai redirect (#11130) 2023-09-04 16:21:37 +00:00
Madhan KumarandAlan d26bab5880 fix: Require an 'Email' placeholder in the 'Add guest' field. (#11123)
* Adding_Email_PlaceHolder_Text

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>

* Adding_Email_PlaceHolder_Text

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>

* Adding_Email_PlaceHolder_Text

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>

* fixes

* fixes

---------

Signed-off-by: Madhan Kumar M <madhankumar26291@gmail.com>
Co-authored-by: Alan <alannnc@gmail.com>
2023-09-04 09:13:12 -07:00
Udit Takkar 1c451a8d51 fix: installed badge (#11131) 2023-09-04 17:51:59 +02:00
Crowdin Bot 576261bf88 New Crowdin translations by Github Action 2023-09-04 15:50:49 +00:00
alannnc 7d944af7f6 fix: teams are listed on insights but then you cant find them on settings page 11087 cal 2401 (#11092)
* fix listing of other teams

* add more invalidate trpc queries

* Remove unused code

* Removed unnecesary flags
2023-09-04 08:47:53 -07:00
Crowdin Bot dfe7056c93 New Crowdin translations by Github Action 2023-09-04 14:55:48 +00:00
b27f01449a feat: make.com/integromat app (#8897)
* start make app integration

* setup integration

* add relevant env vars

* update app metadata

* import setup route in app setups

* fix typo

* add app store imports

* fix module import error

* update make readme

* move scheduler to app-store utils

* move add subscription to node scheduler

* move delete subscription to scheduler

* subscribe unsubscribe in zapier using common nodeScheduler

* fix lint errors

* revert settings.json

* update icon

* add app screenshots

* fix app description

* fix type errors

* update app code

* Delete .gitkeep

* delete unused template files

* get app invite link from env vars

* chore: handle error, cleanup readme, address review comments

* fix: update link in readme

* revert yarn.lock

* fix type errors

* Update packages/prisma/seed-app-store.ts

* Update .env.appStore.example

* Update .env.appStore.example

* update app readme

* fix param name in deleteSubcription

* fix listBookings handler

* Update turbo.json

* use default installation handler to install app

* use logger for console logs

* Fix inviteLink reading

* fix app setup handler

* Fix type issue

* always show app invite link

* fix type error

* add make invite link

---------

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2023-09-04 14:52:34 +00:00
Udit Takkar 6dd77d0494 fix: create event type (#11046) 2023-09-04 15:02:30 +02:00
f73d38b632 fix: color tokens are not used in payment form (#10806)
Co-authored-by: VIneet Pradhan <138296762+vineetpradhan-dev@users.noreply.github.com>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-04 15:02:21 +02:00
Gaurav SutharandUdit Takkar a7d8f0c84e fix: add loading to delete event dialog (#11102)
* add loading to delete event dialog

* chore: use mutation isLoading

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-04 14:59:50 +02:00
Heath Henley b0164990c0 Conformation --> Confirmation (#11058) 2023-09-04 14:56:46 +02:00
Ikko Eltociear Ashimine bad72ba7bf chore: fix typo in embed-iframe.ts (#10858)
currenty -> currently
2023-09-04 17:48:42 +05:30
Crowdin Bot 1b317a98a4 New Crowdin translations by Github Action 2023-09-04 12:17:54 +00:00
Omar López 6a22d75caa fix: no-scrollbar for firefox (#10964) 2023-09-04 17:43:32 +05:30
a030861423 feat: recently added apps (#11118)
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-04 10:30:21 +00:00
omahs bd9cd00c04 fix: typos (#11125) 2023-09-04 12:18:02 +02:00
Kushagra MathurandPeer Richelsen 6fd34af6f6 fix: dark mode icons for Riverside and Sirius Video (#11107)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-04 09:55:16 +00:00
1a85f336c5 fix: Booking Question Dialog: Dropdown covered by DialogFooter (CALCOM-10987) (#10989)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-04 11:42:10 +02:00
Peer Richelsen cc5096a54a chore: stop using telemetry for website (#11050) 2023-09-01 16:53:08 -07:00
alannnc eea7059226 Add test for different layouts (#11091) 2023-09-01 16:26:45 -07:00
Omar López 63f3247b1b fix: Vercel preview rewrites 404s (#11090) 2023-09-01 23:36:59 +02:00
Hariom BalharaandJoe Au-Yeung 7a769c38fb Remove duplicate fields (#11003)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2023-09-02 00:58:21 +05:30
alannnc 34c2565c70 fix paypal minor issues (#11069) 2023-09-01 11:03:27 -07:00
Crowdin Bot a130804f18 New Crowdin translations by Github Action 2023-09-01 17:55:12 +00:00
Leo Giovanetti 446e851065 fix: support subteam in org redirection (#11080) 2023-09-01 19:52:27 +02:00
Abhilash Sampath f06b4bfb9c fix: Add send sms description (#11063) 2023-09-01 19:12:10 +02:00
Alex van Andel 1896efa3a0 v3.2.7.1
release.yml / release (push) Canceled after 0s
2023-09-01 17:45:59 +01:00
Alex van Andel 62c8603863 fix: Revert #10366: Undoing then redo fix differently (#11078)
* Revert #10366: Undoing then redo fix differently

* Re-implement defaulting to first day in the month

* Update Embed.tsx and DateOverrideInputDialog
2023-09-01 17:12:01 +01:00
7d8559b0b0 chore: add Dropdown in storybook (#10874)
Co-authored-by: gitstart-calcom <gitstart@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-09-01 18:08:34 +02:00
sean-brydon 7a48bbe553 Remove background colour (#11082) 2023-09-01 15:55:38 +00:00
nidhi sharma 817e95c246 fix #11077 space added on team events (#11081) 2023-09-01 15:30:37 +00:00
Udit Takkar 4ac6225a35 fix: unauthorized error in organization (#11039)
* fix: unauthorized error in organization

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* chore

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

* fix: allow instance admin to update

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>

---------

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
2023-09-01 14:02:58 +00:00
sean-brydon 38d9b93dee Fix avatar (#11076) 2023-09-01 14:23:28 +01:00
sean-brydonandUdit Takkar 376b38d4ed fix: Fixes username invite issue (#10998)
* Fixes username invite issue

* Ensure we only suggest email invites in org members

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2023-09-01 17:31:45 +05:30
Hariom Balhara 0e181f6d9f Revert "feat: adds next cold start profiler (#11014)" (#11072)
This reverts commit 05631d093f.
2023-09-01 10:18:17 +01:00
175 changed files with 1960 additions and 866 deletions
+4 -5
View File
@@ -1,6 +1,6 @@
# Contributing to Cal.com
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
Contributions are what makes the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
- Before jumping into a PR be sure to search [existing PRs](https://github.com/calcom/cal.com/pulls) or [issues](https://github.com/calcom/cal.com/issues) for an open or closed item that relates to your submission.
@@ -37,7 +37,7 @@ Contributions are what make the open source community such an amazing place to l
</tr>
<tr>
<td>
Core Features (Booking page, availabilty, timezone calculation)
Core Features (Booking page, availability, timezone calculation)
</td>
<td>
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+sort:updated-desc+label:%22High+priority%22">
@@ -132,7 +132,6 @@ If you get errors, be sure to fix them before committing.
## Making a Pull Request
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating you PR.
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue
](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
- Be sure to [check the "Allow edits from maintainers" option](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork) while creating your PR.
- If your PR refers to or fixes an issue, be sure to add `refs #XXX` or `fixes #XXX` to the PR description. Replacing `XXX` with the respective issue number. See more about [Linking a pull request to an issue](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
- Be sure to fill the PR Template accordingly.
+3 -3
View File
@@ -423,7 +423,7 @@ yarn seed-app-store
```
You will need to complete a few more steps to activate Google Calendar App.
Make sure to complete section "Obtaining the Google API Credentials". After the do the
Make sure to complete section "Obtaining the Google API Credentials". After that do the
following
1. Add extra redirect URL `<Cal.com URL>/api/auth/callback/google`
@@ -449,8 +449,8 @@ following
7. Click "Create".
8. Now copy the Client ID and Client Secret to your `.env` file into the `ZOOM_CLIENT_ID` and `ZOOM_CLIENT_SECRET` fields.
9. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zoomvideo/callback` replacing Cal.com URL with the URI at which your application runs.
10. Also add the redirect URL given above as a allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
11. You don't need to provide basic information about your app. Instead click at "Scopes" and then at "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
10. Also add the redirect URL given above as an allow list URL and enable "Subdomain check". Make sure, it says "saved" below the form.
11. You don't need to provide basic information about your app. Instead click on "Scopes" and then on "+ Add Scopes". On the left, click the category "Meeting" and check the scope `meeting:write`.
12. Click "Done".
13. You're good to go. Now you can easily add your Zoom integration in the Cal.com settings.
+9
View File
@@ -5,6 +5,15 @@ plugins.push(withBundleAnalyzer({ enabled: process.env.ANALYZE === "true" }));
/** @type {import("next").NextConfig} */
const nextConfig = {
async redirects() {
return [
{
source: "/",
destination: "https://cal.com/ai",
permanent: true,
},
];
},
i18n: {
defaultLocale: "en",
locales: ["en"],
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/ai",
"version": "0.1.0",
"version": "1.0.1",
"private": true,
"author": "Cal.com Inc.",
"dependencies": {
+11 -8
View File
@@ -45,6 +45,7 @@ export const POST = async (request: NextRequest) => {
select: {
email: true,
id: true,
timeZone: true,
credentials: {
select: {
appId: true,
@@ -55,15 +56,17 @@ export const POST = async (request: NextRequest) => {
where: { email: envelope.from },
});
if (!signature || !user?.email || !user?.id) {
// User is not a cal.com user or is using an unverified email.
if (!signature || !user) {
await sendEmail({
html: `Thanks for your interest in Cal AI! To get started, Make sure you have a <a href="https://cal.com/signup" target="_blank">cal.com</a> account with this email address.`,
subject: `Re: ${body.subject}`,
text: "Sorry, you are not authorized to use this service. Please verify your email address and try again.",
to: user?.email || "",
text: `Thanks for your interest in Cal AI! To get started, Make sure you have a cal.com account with this email address. You can sign up for an account at: https://cal.com/signup`,
to: envelope.from,
from: aiEmail,
});
return new NextResponse();
return new NextResponse("ok");
}
const credential = user.credentials.find((c) => c.appId === env.APP_ID)?.key;
@@ -93,8 +96,8 @@ export const POST = async (request: NextRequest) => {
fetchAvailability({
apiKey,
userId: user.id,
dateFrom: now,
dateTo: now,
dateFrom: now(user.timeZone),
dateTo: now(user.timeZone),
}),
]);
@@ -120,7 +123,7 @@ export const POST = async (request: NextRequest) => {
return new NextResponse("Error fetching event types. Please try again.", { status: 400 });
}
const { timeZone, workingHours } = availability;
const { workingHours } = availability;
const appHost = getHostFromHeaders(request.headers);
@@ -135,7 +138,7 @@ export const POST = async (request: NextRequest) => {
user: {
email: user.email,
eventTypes,
timeZone,
timeZone: user.timeZone,
workingHours,
},
}),
@@ -52,9 +52,8 @@ const createBooking = async ({
method: "POST",
});
if (response.status === 401) {
throw new Error("Unauthorized");
}
// Let GPT handle this. This will happen when wrong event type id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
+2 -1
View File
@@ -31,7 +31,8 @@ const cancelBooking = async ({
method: "DELETE",
});
if (response.status === 401) throw new Error("Unauthorized");
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
+1 -3
View File
@@ -35,9 +35,7 @@ export const fetchAvailability = async ({
const response = await fetch(url);
if (response.status === 401) {
throw new Error("Unauthorized");
}
if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
+2 -1
View File
@@ -41,7 +41,8 @@ const editBooking = async ({
method: "PATCH",
});
if (response.status === 401) throw new Error("Unauthorized");
// Let GPT handle this. This will happen when wrong booking id is used.
// if (response.status === 401) throw new Error("Unauthorized");
const data = await response.json();
+1 -1
View File
@@ -44,7 +44,7 @@ const agent = async (input: string, user: User, apiKey: string, userId: number)
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 the user should be formatted per that user's timezone.
Current UTC time is: ${now}
The current time in the user's timezone is: ${now(user.timeZone)}
The user's time zone is: ${user.timeZone}
The user's event types are: ${user.eventTypes
.map((e: EventType) => `ID: ${e.id}, Title: ${e.title}, Length: ${e.length}`)
+5 -1
View File
@@ -1 +1,5 @@
export default new Date().toISOString();
export default function now(timeZone: string) {
return new Date().toLocaleString("en-US", {
timeZone,
});
}
+1 -1
View File
@@ -13,7 +13,7 @@ import { Meta } from "@storybook/addon-docs";
<a href="https://www.figma.com/file/9MOufQNLtdkpnDucmNX10R/%E2%9D%96-Cal-DS" target="_blank">
Figma
</a>{" "}
library is avalible for anyone to view and use. If you have any questions or concerns, please reach out to
library is available for anyone to view and use. If you have any questions or concerns, please reach out to
the design team.
</p>
</div>
+8 -1
View File
@@ -4,6 +4,7 @@ import { useEffect, useRef, useState } from "react";
import { z } from "zod";
import type { CredentialOwner } from "@calcom/app-store/types";
import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
@@ -80,7 +81,13 @@ export default function AppListCard(props: AppListCardProps) {
return (
<div className={`${highlight ? "dark:bg-muted bg-yellow-100" : ""}`}>
<div className="flex items-center gap-x-3 px-5 py-4">
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
{logo ? (
<img
className={classNames(logo.includes("-dark") && "dark:invert", "h-10 w-10")}
src={logo}
alt={`${title} logo`}
/>
) : null}
<div className="flex grow flex-col gap-y-1 truncate">
<div className="flex items-center gap-x-2">
<h3 className="text-emphasis truncate text-sm font-semibold">{title}</h3>
@@ -306,7 +306,7 @@ export const EventSetupTab = (
"h-4 w-4",
// invert all the icons except app icons
eventLocationType.iconUrl &&
!eventLocationType.iconUrl.startsWith("/app-store") &&
eventLocationType.iconUrl.includes("-dark") &&
"dark:invert"
)}
alt={`${eventLocationType.label} logo`}
@@ -66,6 +66,7 @@ type Props = {
formMethods: UseFormReturn<FormValues>;
isUpdateMutationLoading?: boolean;
availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean;
};
function getNavigation(props: {
@@ -133,6 +134,7 @@ function EventTypeSingleLayout({
isUpdateMutationLoading,
formMethods,
availability,
isUserOrganizationAdmin,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
@@ -142,7 +144,8 @@ function EventTypeSingleLayout({
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED;
eventType.schedulingType === SchedulingType.MANAGED ||
isUserOrganizationAdmin;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
onSuccess: async () => {
@@ -98,14 +98,7 @@ const UserProfile = () => {
return (
<form onSubmit={onSubmit}>
<div className="flex flex-row items-center justify-start rtl:justify-end">
{user && (
<Avatar
alt={user.username || "user avatar"}
gravatarFallbackMd5={user.emailMd5}
size="lg"
imageSrc={imageSrc}
/>
)}
{user && <Avatar alt={user.username || "user avatar"} size="lg" imageSrc={imageSrc} />}
<input
ref={avatarRef}
type="hidden"
@@ -4,7 +4,6 @@ import { components } from "react-select";
import type { EventLocationType } from "@calcom/app-store/locations";
import type { CredentialDataWithTeamName } from "@calcom/app-store/utils";
import { classNames } from "@calcom/lib";
import cx from "@calcom/lib/classNames";
import { Select } from "@calcom/ui";
export type LocationOption = {
@@ -28,7 +27,7 @@ const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
src={icon}
alt="cover"
// invert all the icons except app icons
className={cx("h-3.5 w-3.5", icon && !icon.startsWith("/app-store") && "dark:invert")}
className={classNames(icon.includes("-dark") && "dark:invert", "h-3.5 w-3.5")}
/>
)}
<span className={classNames("text-sm font-medium")}>{label}</span>
@@ -57,7 +56,13 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
}}
formatOptionLabel={(e) => (
<div className="flex items-center gap-3">
{e.icon && <img src={e.icon} alt="app-icon" className="h-5 w-5" />}
{e.icon && (
<img
src={e.icon}
alt="app-icon"
className={classNames(e.icon.includes("-dark") && "dark:invert", "h-5 w-5")}
/>
)}
<span>{e.label}</span>
</div>
)}
+1 -1
View File
@@ -32,7 +32,7 @@ function getCspPolicy(nonce: string) {
IS_PRODUCTION ? (useNonStrictPolicy ? "'unsafe-inline'" : "") : "'unsafe-inline'"
} app.cal.com;
font-src 'self';
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
img-src 'self' ${WEBAPP_URL} https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
connect-src 'self'
`;
}
-11
View File
@@ -1,11 +0,0 @@
import crypto from "crypto";
export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) {
if (!email && !md5) return "";
if (email && !md5) {
md5 = crypto.createHash("md5").update(email).digest("hex");
}
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
};
+7
View File
@@ -450,6 +450,13 @@ const nextConfig = {
},
{
source: "/support",
missing: [
{
type: "header",
key: "host",
value: orgHostPath,
},
],
destination: "/event-types?openIntercom=true",
permanent: true,
},
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.2.7",
"version": "3.2.8",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+2 -9
View File
@@ -1,13 +1,11 @@
import crypto from "crypto";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains";
import { AVATAR_FALLBACK } from "@calcom/lib/constants";
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import prisma from "@calcom/prisma";
import { defaultAvatarSrc } from "@lib/profile";
const querySchema = z
.object({
username: z.string(),
@@ -75,12 +73,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
res.setHeader("x-cal-org", identity.org);
}
res.writeHead(302, {
Location: defaultAvatarSrc({
md5: crypto
.createHash("md5")
.update(identity?.email || "guest@example.com")
.digest("hex"),
}),
Location: AVATAR_FALLBACK,
});
return res.end();
+9 -1
View File
@@ -12,7 +12,14 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { AppCategories } from "@calcom/prisma/enums";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import type { HorizontalTabItemProps } from "@calcom/ui";
import { AllApps, AppStoreCategories, HorizontalTabs, TextField, PopularAppsSlider } from "@calcom/ui";
import {
AllApps,
AppStoreCategories,
HorizontalTabs,
TextField,
PopularAppsSlider,
RecentAppsSlider,
} from "@calcom/ui";
import { Search } from "@calcom/ui/components/icon";
import PageWrapper from "@components/PageWrapper";
@@ -81,6 +88,7 @@ export default function Apps({
<>
<AppStoreCategories categories={categories} />
<PopularAppsSlider items={appStore} />
<RecentAppsSlider items={appStore} />
</>
)}
<AllApps
+8 -1
View File
@@ -52,6 +52,8 @@ export default function Login({
totpEmail,
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
const searchParams = useSearchParams();
const isTeamInvite = searchParams.get("teamInvite");
const { t } = useLocale();
const router = useRouter();
const formSchema = z
@@ -95,7 +97,9 @@ export default function Login({
callbackUrl = safeCallbackUrl || "";
const LoginFooter = (
<a href={`${WEBSITE_URL}/signup`} className="text-brand-500 font-medium">
<a
href={callbackUrl !== "" ? `${WEBSITE_URL}/signup?callbackUrl=${callbackUrl}` : `${WEBSITE_URL}/signup`}
className="text-brand-500 font-medium">
{t("dont_have_an_account")}
</a>
);
@@ -184,6 +188,9 @@ export default function Login({
? LoginFooter
: null
}>
{isTeamInvite && (
<Alert severity="info" message={t("signin_or_signup_to_accept_invite")} className="mb-4 mt-4" />
)}
<FormProvider {...methods}>
<form onSubmit={methods.handleSubmit(onSubmit)} noValidate data-testid="login-form">
<div>
+2 -1
View File
@@ -443,7 +443,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
isUpdateMutationLoading={updateMutation.isLoading}
formMethods={formMethods}
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
currentUserMembership={currentUserMembership}>
currentUserMembership={currentUserMembership}
isUserOrganizationAdmin={props.isUserOrganizationAdmin}>
<Form
form={formMethods}
id="event-type-form"
+3 -2
View File
@@ -402,7 +402,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
)}
/>
)}
{isManagedEventType && (
{isManagedEventType && type.children && (
<AvatarGroup
className="relative right-3 top-1"
size="sm"
@@ -662,6 +662,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
title={t(`delete${isManagedEventPrefix()}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
isLoading={deleteMutation.isLoading}
onConfirm={(e) => {
e.preventDefault();
deleteEventTypeHandler(deleteDialogTypeId);
@@ -873,7 +874,7 @@ const Main = ({
<MobileTeamsTab eventTypeGroups={data.eventTypeGroups} />
) : (
data.eventTypeGroups.map((group: EventTypeGroup, index: number) => (
<div className="flex flex-col" key={group.profile.slug}>
<div className="mt-4 flex flex-col" key={group.profile.slug}>
<EventTypeListHeading
profile={group.profile}
membershipCount={group.metadata.membershipCount}
@@ -406,7 +406,7 @@ const ProfileForm = ({
name="avatar"
render={({ field: { value } }) => (
<>
<Avatar alt="" imageSrc={value} gravatarFallbackMd5="fallback" size="lg" />
<Avatar alt="" imageSrc={value} size="lg" />
<div className="ms-4">
<ImageUploader
target="avatar"
+22 -8
View File
@@ -13,6 +13,7 @@ import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { useFlagMap } from "@calcom/features/flags/context/provider";
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
import { IS_SELF_HOSTED, WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
@@ -34,6 +35,23 @@ type FormValues = z.infer<typeof signupSchema>;
type SignupProps = inferSSRProps<typeof getServerSideProps>;
const getSafeCallbackUrl = (url: string | null) => {
if (!url) return null;
let callbackUrl = url;
if (/"\//.test(callbackUrl)) callbackUrl = callbackUrl.substring(1);
// If not absolute URL, make it absolute
if (!/^https?:\/\//.test(callbackUrl)) {
callbackUrl = `${WEBAPP_URL}/${callbackUrl}`;
}
const safeCallbackUrl = getSafeRedirectUrl(callbackUrl);
return safeCallbackUrl;
};
export default function Signup({ prepopulateFormValues, token, orgSlug }: SignupProps) {
const searchParams = useSearchParams();
const telemetry = useTelemetry();
@@ -55,6 +73,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
throw new Error(err.message);
}
};
const callbackUrl = getSafeCallbackUrl(searchParams.get("callbackUrl"));
const signUp: SubmitHandler<FormValues> = async (data) => {
await fetch("/api/auth/signup", {
@@ -72,13 +91,10 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
.then(async () => {
telemetry.event(telemetryEventTypes.signup, collectPageParameters());
const verifyOrGettingStarted = flags["email-verification"] ? "auth/verify-email" : "getting-started";
await signIn<"credentials">("credentials", {
...data,
callbackUrl: `${
searchParams?.get("callbackUrl")
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
: `${WEBAPP_URL}/${verifyOrGettingStarted}`
}?from=signup`,
callbackUrl: `${callbackUrl ? callbackUrl : `${WEBAPP_URL}/${verifyOrGettingStarted}`}?from=signup`,
});
})
.catch((err) => {
@@ -157,9 +173,7 @@ export default function Signup({ prepopulateFormValues, token, orgSlug }: Signup
className="w-full justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: searchParams?.get("callbackUrl")
? `${WEBAPP_URL}/${searchParams.get("callbackUrl")}`
: `${WEBAPP_URL}/getting-started`,
callbackUrl: callbackUrl ? callbackUrl : `${WEBAPP_URL}/getting-started`,
})
}>
{t("login_instead")}
+17
View File
@@ -1,9 +1,11 @@
import type { GetServerSidePropsContext } from "next";
import { getLayout } from "@calcom/features/MainLayout";
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
import { TeamsListing } from "@calcom/features/ee/teams/components";
import { ShellMain } from "@calcom/features/shell/Shell";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
@@ -41,6 +43,21 @@ function Teams() {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
const session = await getServerSession({ req: context.req, res: context.res });
const token = context.query?.token;
const resolvedUrl = context.resolvedUrl;
const callbackUrl = token ? getSafeRedirectUrl(`${WEBAPP_URL}${resolvedUrl}`) : null;
if (!session) {
return {
redirect: {
destination: callbackUrl ? `/auth/login?callbackUrl=${callbackUrl}&teamInvite=true` : "/auth/login",
permanent: false,
},
props: {},
};
}
return { props: { trpcState: ssr.dehydrate() } };
};
+1 -1
View File
@@ -25,7 +25,7 @@ const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
process.env.NEXT_PUBLIC_WEBAPP_URL || "https://" + process.env.VERCEL_URL
));
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\..*`;
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`;
let beforeRewriteExcludePages = pages.concat(otherNonExistingRoutePrefixes);
exports.orgUserRoutePath = `/:user((?!${beforeRewriteExcludePages.join("|")}|_next|public)[a-zA-Z0-9\-_]+)`;
+57
View File
@@ -268,3 +268,60 @@ test.describe("prefill", () => {
});
});
});
test.describe("Booking on different layouts", () => {
test.beforeEach(async ({ page, users }) => {
const user = await users.create();
await page.goto(`/${user.username}`);
});
test("Book on week layout", async ({ page }) => {
// Click first event type
await page.click('[data-testid="event-type-link"]');
await page.click('[data-testid="toggle-group-item-week_view"]');
await page.click('[data-testid="incrementMonth"]');
await page.locator('[data-testid="calendar-empty-cell"]').nth(0).click();
// Fill what is this meeting about? name email and notes
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
await page.locator('[name="notes"]').fill("Test notes");
await page.click('[data-testid="confirm-book-button"]');
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
// expect page to be booking page
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
test("Book on column layout", async ({ page }) => {
// Click first event type
await page.click('[data-testid="event-type-link"]');
await page.click('[data-testid="toggle-group-item-column_view"]');
await page.click('[data-testid="incrementMonth"]');
await page.locator('[data-testid="time"]').nth(0).click();
// Fill what is this meeting about? name email and notes
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
await page.locator('[name="notes"]').fill("Test notes");
await page.click('[data-testid="confirm-book-button"]');
await page.waitForURL((url) => {
return url.pathname.startsWith("/booking");
});
// expect page to be booking page
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
});
@@ -0,0 +1,61 @@
import { v4 as uuidv4 } from "uuid";
import { prisma } from "@calcom/prisma";
type Route = {
id: string;
action: {
type: string;
value: string;
};
isFallback: boolean;
queryValue: {
id: string;
type: string;
};
};
export const createRoutingFormsFixture = () => {
return {
async create({
userId,
teamId,
name,
fields,
routes = [],
}: {
name: string;
userId: number;
teamId: number | null;
routes?: Route[];
fields: {
type: string;
label: string;
identifier?: string;
required: boolean;
}[];
}) {
return await prisma.app_RoutingForms_Form.create({
data: {
name,
userId,
teamId,
routes: [
...routes,
// Add a fallback route always, this is taken care of tRPC route normally but do it manually while running the query directly.
{
id: "898899aa-4567-489a-bcde-f1823f708646",
action: { type: "customPageMessage", value: "Fallback Message" },
isFallback: true,
queryValue: { id: "898899aa-4567-489a-bcde-f1823f708646", type: "group" },
},
],
fields: fields.map((f) => ({
id: uuidv4(),
...f,
})),
},
});
},
};
};
+5
View File
@@ -10,6 +10,7 @@ import type { ExpectedUrlDetails } from "../../../../playwright.config";
import { createBookingsFixture } from "../fixtures/bookings";
import { createEmbedsFixture, createGetActionFiredDetails } from "../fixtures/embeds";
import { createPaymentsFixture } from "../fixtures/payments";
import { createRoutingFormsFixture } from "../fixtures/routingForms";
import { createServersFixture } from "../fixtures/servers";
import { createUsersFixture } from "../fixtures/users";
@@ -23,6 +24,7 @@ export interface Fixtures {
servers: ReturnType<typeof createServersFixture>;
prisma: typeof prisma;
emails?: API;
routingForms: ReturnType<typeof createRoutingFormsFixture>;
}
declare global {
@@ -71,6 +73,9 @@ export const test = base.extend<Fixtures>({
prisma: async ({}, use) => {
await use(prisma);
},
routingForms: async ({}, use) => {
await use(createRoutingFormsFixture());
},
emails: async ({}, use) => {
if (IS_MAILHOG_ENABLED) {
const mailhogAPI = mailhog();
+113 -33
View File
@@ -1,3 +1,4 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
@@ -9,6 +10,9 @@ import {
gotoRoutingLink,
} from "./lib/testUtils";
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
test.afterEach(({ users }) => users.deleteAll());
test.describe("BOOKING_CREATED", async () => {
@@ -55,8 +59,6 @@ test.describe("BOOKING_CREATED", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body: any = request.body;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
@@ -187,8 +189,6 @@ test.describe("BOOKING_REJECTED", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
@@ -311,8 +311,6 @@ test.describe("BOOKING_REQUESTED", async () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
@@ -391,54 +389,136 @@ test.describe("BOOKING_REQUESTED", async () => {
});
test.describe("FORM_SUBMITTED", async () => {
test("can submit a form and get a submission event", async ({ page, users }) => {
test("on submitting user form, triggers user webhook", async ({ page, users, routingForms }) => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const user = await users.create(null, {
hasTeam: true,
});
await user.apiLogin();
await page.goto("/settings/teams/new");
await page.waitForLoadState("networkidle");
const teamName = `${user.username}'s Team`;
// Create a new team
await page.locator('input[name="name"]').fill(teamName);
await page.locator('input[name="slug"]').fill(teamName);
await page.locator('button[type="submit"]').click();
await page.locator("text=Publish team").click();
await page.waitForURL(/\/settings\/teams\/(\d+)\/profile$/i);
await page.waitForLoadState("networkidle");
await page.waitForLoadState("networkidle");
await page.goto(`/settings/developer/webhooks/new`);
// Add webhook
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([page.click("[type=submit]"), page.goForward()]);
await page.click("[type=submit]");
// Page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
await page.waitForLoadState("networkidle");
await page.goto("/routing-forms/forms");
await page.click('[data-testid="new-routing-form"]');
// Choose to create the Form for the user(which is the first option) and not the team
await page.click('[data-testid="option-0"]');
await page.fill("input[name]", "TEST FORM");
await page.click('[data-testid="add-form"]');
await page.waitForSelector('[data-testid="add-field"]');
const url = page.url();
const formId = new URL(url).pathname.split("/").at(-1);
const form = await routingForms.create({
name: "Test Form",
userId: user.id,
teamId: null,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
await gotoRoutingLink({ page, formId: formId });
await gotoRoutingLink({ page, formId: form.id });
const fieldName = "name";
await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe");
page.click('button[type="submit"]');
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
expect(body).toEqual({
triggerEvent: "FORM_SUBMITTED",
createdAt: dynamic,
payload: {
formId: form.id,
formName: form.name,
teamId: null,
responses: {
name: {
value: "John Doe",
},
},
},
name: "John Doe",
});
webhookReceiver.close();
});
test("on submitting team form, triggers team webhook", async ({ page, users, routingForms }) => {
const webhookReceiver = createHttpServer();
const user = await users.create(null, {
hasTeam: true,
});
await user.apiLogin();
await page.goto(`/settings/developer/webhooks`);
const teamId = await clickFirstTeamWebhookCta(page);
// Add webhook
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await page.click("[type=submit]");
const form = await routingForms.create({
name: "Test Form",
userId: user.id,
teamId: teamId,
fields: [
{
type: "text",
label: "Name",
identifier: "name",
required: true,
},
],
});
await gotoRoutingLink({ page, formId: form.id });
const fieldName = "name";
await page.fill(`[data-testid="form-field-${fieldName}"]`, "John Doe");
page.click('button[type="submit"]');
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const body = request.body as any;
body.createdAt = dynamic;
expect(body).toEqual({
triggerEvent: "FORM_SUBMITTED",
createdAt: dynamic,
payload: {
formId: form.id,
formName: form.name,
teamId,
responses: {
name: {
value: "John Doe",
},
},
},
name: "John Doe",
});
webhookReceiver.close();
});
});
async function clickFirstTeamWebhookCta(page: Page) {
await page.click('[data-testid="new_webhook"]');
await page.click('[data-testid="option-team-1"]');
await page.waitForURL((u) => u.pathname === "/settings/developer/webhooks/new");
const url = page.url();
const teamId = Number(new URL(url).searchParams.get("teamId")) as number;
return teamId;
}
+1
View File
@@ -0,0 +1 @@
<svg width="32" height="32" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#a)"><rect width="32" height="32" rx="16" fill="#E5E7EB"/><path d="M16 21.333A6.667 6.667 0 1 0 16 8a6.667 6.667 0 0 0 0 13.333Z" fill="#1F2937"/><path d="M26.667 32a10.667 10.667 0 1 0-21.334 0" fill="#1F2937"/><path d="M16 21.333A6.667 6.667 0 1 0 16 8a6.667 6.667 0 0 0 0 13.333Zm0 0A10.667 10.667 0 0 1 26.667 32M16 21.333A10.666 10.666 0 0 0 5.333 32" stroke="#1F2937" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></g><defs><clipPath id="a"><rect width="32" height="32" rx="16" fill="#fff"/></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 635 B

@@ -1074,7 +1074,7 @@
"event_cancelled": "تم إلغاء هذا الحدث",
"emailed_information_about_cancelled_event": "لقد أرسلنا إليك وإلى الحاضرين الآخرين بريداً إلكترونياً لإعلامهم.",
"this_input_will_shown_booking_this_event": "سيُعرَض هذا المُدخَل عند حجز هذا الحدث",
"meeting_url_in_conformation_email": "عنوان رابط الاجتماع موجود في رسالة التأكيد الإلكترونية",
"meeting_url_in_confirmation_email": "عنوان رابط الاجتماع موجود في رسالة التأكيد الإلكترونية",
"url_start_with_https": "يجب أن يبدأ العنوان بـ http:// أو https://",
"number_provided": "سيتم توفير رقم الهاتف",
"before_event_trigger": "قبل بدء الحدث",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Tato událost je zrušena",
"emailed_information_about_cancelled_event": "Poslali jsme vám a ostatním účastníkům informační e-mail.",
"this_input_will_shown_booking_this_event": "Toto pole se zobrazí při rezervaci této události",
"meeting_url_in_conformation_email": "Adresa URL schůzky je v potvrzovacím e-mailu",
"meeting_url_in_confirmation_email": "Adresa URL schůzky je v potvrzovacím e-mailu",
"url_start_with_https": "Adresa URL musí začínat http:// nebo https://",
"number_provided": "Bude uvedeno telefonní číslo",
"before_event_trigger": "před zahájením události",
@@ -960,7 +960,7 @@
"reschedule_placeholder": "Lad andre vide, hvorfor du har brug for at omlægge",
"emailed_information_about_cancelled_event": "Vi har sendt en e-mail til dig og de andre deltagere for at give dem besked.",
"this_input_will_shown_booking_this_event": "Dette input vil blive vist ved booking af denne begivenhed",
"meeting_url_in_conformation_email": "Møde-url er i bekræftelses-mailen",
"meeting_url_in_confirmation_email": "Møde-url er i bekræftelses-mailen",
"url_start_with_https": "URL'en skal starte med http:// eller https://",
"number_provided": "Telefonnummer vil blive angivet",
"before_event_trigger": "før begivenheden starter",
@@ -1095,7 +1095,7 @@
"event_cancelled": "Dieser Termin ist abgesagt",
"emailed_information_about_cancelled_event": "Wir haben Ihnen und den anderen Teilnehmern eine E-Mail gesendet, damit alle Bescheid wissen.",
"this_input_will_shown_booking_this_event": "Diese Eingabe wird bei der Buchung dieses Termins angezeigt",
"meeting_url_in_conformation_email": "Termin-URL ist in der Bestätigungsmail",
"meeting_url_in_confirmation_email": "Termin-URL ist in der Bestätigungsmail",
"url_start_with_https": "URL muss mit http:// oder https:// beginnen",
"number_provided": "Telefonnummer wird angegeben",
"before_event_trigger": "vor Beginn des Termins",
@@ -45,6 +45,7 @@
"invite_team_individual_segment": "Invite individual",
"invite_team_bulk_segment": "Bulk import",
"invite_team_notifcation_badge": "Inv.",
"signin_or_signup_to_accept_invite": "You need to Sign in or Sign up to see team invitation.",
"your_event_has_been_scheduled": "Your event has been scheduled",
"your_event_has_been_scheduled_recurring": "Your recurring event has been scheduled",
"accept_our_license": "Accept our license by changing the .env variable <1>NEXT_PUBLIC_LICENSE_CONSENT</1> to '{{agree}}'.",
@@ -1053,12 +1054,15 @@
"how_you_want_add_cal_site": "How do you want to add {{appName}} to your site?",
"choose_ways_put_cal_site": "Choose one of the following ways to put {{appName}} on your site.",
"setting_up_zapier": "Setting up your Zapier integration",
"setting_up_make": "Setting up your Make integration",
"generate_api_key": "Generate API key",
"generate_api_key_description": "Generate an API key to use with {{appName}} at",
"your_unique_api_key": "Your unique API key",
"copy_safe_api_key": "Copy this API key and save it somewhere safe. If you lose this key you have to generate a new one.",
"zapier_setup_instructions": "<0>Log into your Zapier account and create a new Zap.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"make_setup_instructions": "<0>Log into your Make account and create a new Scenario.</0><1>Select Cal.com as your Trigger app. Also choose a Trigger event.</1><2>Choose your account and then enter your Unique API Key.</2><3>Test your Trigger.</3><4>You're set!</4>",
"install_zapier_app": "Please first install the Zapier App in the app store.",
"install_make_app": "Please first install the Make App in the app store.",
"connect_apple_server": "Connect to Apple Server",
"calendar_url": "Calendar URL",
"apple_server_generate_password": "Generate an app specific password to use with {{appName}} at",
@@ -1089,6 +1093,7 @@
"or_lowercase": "or",
"nevermind": "Nevermind",
"go_to": "Go to: ",
"invite_link_make": "<li>Go to<a>Make Invite Link</a> and install Cal.com app</li>",
"zapier_invite_link": "Zapier Invite Link",
"meeting_url_provided_after_confirmed": "A Meeting URL will be created once the event is confirmed.",
"dynamically_display_attendee_or_organizer": "Dynamically display the name of your attendee for you, or your name if it's viewed by your attendee",
@@ -1098,7 +1103,7 @@
"event_cancelled": "This event is canceled",
"emailed_information_about_cancelled_event": "We emailed you and the other attendees to let them know.",
"this_input_will_shown_booking_this_event": "This input will be shown when booking this event",
"meeting_url_in_conformation_email": "Meeting url is in the confirmation email",
"meeting_url_in_confirmation_email": "Meeting url is in the confirmation email",
"url_start_with_https": "URL needs to start with http:// or https://",
"number_provided": "Phone number will be provided",
"before_event_trigger": "before event starts",
@@ -1108,6 +1113,7 @@
"email_attendee_action": "send email to attendees",
"sms_attendee_action": "Send SMS to attendee",
"sms_number_action": "send SMS to a specific number",
"send_reminder_sms": "Easily send meeting reminders via SMS to your attendees",
"whatsapp_number_action": "send WhatsApp message to a specific number",
"whatsapp_attendee_action": "send WhatsApp message to attendee",
"workflows": "Workflows",
@@ -2036,5 +2042,7 @@
"team_no_event_types": "This team has no event types",
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
"include_calendar_event": "Include calendar event",
"recently_added":"Recently added",
"no_members_found": "No members found",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1075,7 +1075,7 @@
"event_cancelled": "Este evento se canceló",
"emailed_information_about_cancelled_event": "Le enviamos un correo electrónico a usted y a los demás asistentes para informarles.",
"this_input_will_shown_booking_this_event": "Esta entrada se mostrará al reservar este evento.",
"meeting_url_in_conformation_email": "La URL de la reunión está en el correo de confirmación",
"meeting_url_in_confirmation_email": "La URL de la reunión está en el correo de confirmación",
"url_start_with_https": "La URL debe empezar por http:// o https://",
"number_provided": "Se proporcionará el número de teléfono",
"before_event_trigger": "antes del inicio del evento",
+11 -3
View File
@@ -45,6 +45,7 @@
"invite_team_individual_segment": "Inviter une personne",
"invite_team_bulk_segment": "Importation multiple",
"invite_team_notifcation_badge": "Inv.",
"signin_or_signup_to_accept_invite": "Vous devez vous connecter ou vous inscrire pour voir l'invitation d'équipe.",
"your_event_has_been_scheduled": "Votre événement a été planifié",
"your_event_has_been_scheduled_recurring": "Votre événement récurrent a été planifié",
"accept_our_license": "Acceptez notre licence en changeant la variable .env <1>NEXT_PUBLIC_LICENSE_CONSENT</1> en « {{agree}} ».",
@@ -1053,12 +1054,15 @@
"how_you_want_add_cal_site": "Comment souhaitez-vous ajouter {{appName}} à votre site ?",
"choose_ways_put_cal_site": "Choisissez l'une des méthodes suivantes pour mettre {{appName}} sur votre site.",
"setting_up_zapier": "Configuration de votre intégration Zapier",
"setting_up_make": "Configuration de votre intégration Make",
"generate_api_key": "Générer une clé API",
"generate_api_key_description": "Générer une clé API à utiliser avec {{appName}} sur",
"your_unique_api_key": "Votre clé API unique",
"copy_safe_api_key": "Copiez cette clé API et conservez-la dans un endroit sûr. Si vous perdez cette clé, vous devrez en générer une nouvelle.",
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application Trigger. Choisissez également un événement Trigger.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre Trigger.</3><4>Vous êtes prêt !</4>",
"zapier_setup_instructions": "<0>Connectez-vous à votre compte Zapier et créez un nouveau Zap.</0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre déclencheur.</3><4>Vous êtes prêt !</4>",
"make_setup_instructions": "<0>Connectez-vous à votre compte Make et créez un nouveau Scénario.</0><1>Sélectionnez Cal.com comme application déclencheur. Choisissez également un événement déclencheur.</1><2>Choisissez votre compte, puis saisissez votre clé API unique.</2><3>Testez votre déclencheur.</3><4>Vous êtes prêt !</4>",
"install_zapier_app": "Veuillez d'abord installer l'application Zapier dans l'App Store.",
"install_make_app": "Veuillez d'abord installer l'application Make dans l'App Store.",
"connect_apple_server": "Se connecter au serveur d'Apple",
"calendar_url": "Lien du calendrier",
"apple_server_generate_password": "Générez un mot de passe pour application à utiliser avec {{appName}} sur",
@@ -1089,6 +1093,7 @@
"or_lowercase": "ou",
"nevermind": "Annuler",
"go_to": "Aller à : ",
"invite_link_make": "<li>Accédez<a>au lien d'invitation Make</a> et installez l'application Cal.com</li>",
"zapier_invite_link": "Lien d'invitation Zapier",
"meeting_url_provided_after_confirmed": "Un lien de rendez-vous sera créé une fois l'événement confirmé.",
"dynamically_display_attendee_or_organizer": "Affiche dynamiquement le nom du participant pour vous ou votre nom s'il est vu par le participant",
@@ -1098,7 +1103,7 @@
"event_cancelled": "Cet événement est annulé",
"emailed_information_about_cancelled_event": "Nous vous avons envoyé un e-mail à vous et aux autres participants pour les en informer.",
"this_input_will_shown_booking_this_event": "Ce champ sera affiché lors de la réservation de cet événement.",
"meeting_url_in_conformation_email": "Le lien du rendez-vous est dans l'e-mail de confirmation",
"meeting_url_in_confirmation_email": "Le lien du rendez-vous est dans l'e-mail de confirmation",
"url_start_with_https": "Le lien doit commencer par http:// ou https://",
"number_provided": "Un numéro de téléphone sera fourni",
"before_event_trigger": "avant le début de l'événement",
@@ -1108,6 +1113,7 @@
"email_attendee_action": "envoyer un e-mail aux participants",
"sms_attendee_action": "Envoyer un SMS au participant",
"sms_number_action": "envoyer un SMS à un numéro spécifique",
"send_reminder_sms": "Envoyez facilement des rappels de rendez-vous par SMS à vos participants",
"whatsapp_number_action": "envoyer un message WhatsApp à un numéro spécifique",
"whatsapp_attendee_action": "envoyer un message WhatsApp au participant",
"workflows": "Workflows",
@@ -1115,7 +1121,7 @@
"add_new_workflow": "Ajouter un nouveau workflow",
"reschedule_event_trigger": "lorsque l'événement est replanifié",
"trigger": "Déclencheur",
"triggers": "Déclencher",
"triggers": "Déclencheurs",
"action": "Action",
"workflows_to_automate_notifications": "Créez des workflows pour automatiser les notifications et les rappels.",
"workflow_name": "Nom du workflow",
@@ -2036,5 +2042,7 @@
"team_no_event_types": "Cette équipe n'a aucun type d'événement",
"seat_options_doesnt_multiple_durations": "L'option par place ne prend pas en charge les durées multiples",
"include_calendar_event": "Inclure l'événement du calendrier",
"recently_added": "Ajouté récemment",
"no_members_found": "Aucun membre trouvé",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Ajoutez vos nouvelles chaînes ci-dessus ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1075,7 +1075,7 @@
"event_cancelled": "האירוע בוטל",
"emailed_information_about_cancelled_event": "שלחנו לך ולשאר המשתתפים הודעת דוא״ל כדי ליידע אותם על כך.",
"this_input_will_shown_booking_this_event": "הקלט יוצג בעת הזמנת אירוע זה",
"meeting_url_in_conformation_email": "כתובת ה-URL של הפגישה נמצאת בהודעת הדוא״ל שנשלחה לאישור הפגישה",
"meeting_url_in_confirmation_email": "כתובת ה-URL של הפגישה נמצאת בהודעת הדוא״ל שנשלחה לאישור הפגישה",
"url_start_with_https": "כתובת ה-URL צריכה להתחיל עם הקידומת http://‎ או https://‎",
"number_provided": "מספר הטלפון יסופק",
"before_event_trigger": "לפני שהאירוע מתחיל",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Questo evento è stato cancellato",
"emailed_information_about_cancelled_event": "Abbiamo inviato un'e-mail di notifica a te e agli altri partecipanti.",
"this_input_will_shown_booking_this_event": "Questo input verrà mostrato durante la prenotazione di questo evento",
"meeting_url_in_conformation_email": "L'URL della riunione è riportato nell'e-mail di conferma",
"meeting_url_in_confirmation_email": "L'URL della riunione è riportato nell'e-mail di conferma",
"url_start_with_https": "L'URL deve iniziare con http:// o https://",
"number_provided": "Verrà fornito il numero di telefono",
"before_event_trigger": "prima dell'inizio dell'evento",
@@ -1075,7 +1075,7 @@
"event_cancelled": "このイベントはキャンセルされました",
"emailed_information_about_cancelled_event": "あなたと他の出席者に電子メールでお知らせしました。",
"this_input_will_shown_booking_this_event": "この入力は、このイベントを予約する際に表示されます",
"meeting_url_in_conformation_email": "ミーティングURLは確認メールに記載されています",
"meeting_url_in_confirmation_email": "ミーティングURLは確認メールに記載されています",
"url_start_with_https": "URLは、http://またはhttps://で始まる必要があります",
"number_provided": "電話番号が提供されます",
"before_event_trigger": "イベント開始前に",
@@ -1075,7 +1075,7 @@
"event_cancelled": "이 이벤트는 취소되었습니다",
"emailed_information_about_cancelled_event": "귀하와 다른 참석자들에게 이메일로 알려 드렸습니다.",
"this_input_will_shown_booking_this_event": "이 입력은 이 이벤트를 예약할 때 표시됩니다.",
"meeting_url_in_conformation_email": "회의 URL은 확인 이메일에 있습니다",
"meeting_url_in_confirmation_email": "회의 URL은 확인 이메일에 있습니다",
"url_start_with_https": "URL은 http:// 또는 https://로 시작해야 합니다",
"number_provided": "전화 번호는 제공됩니다",
"before_event_trigger": "이벤트 시작 전",
@@ -1079,7 +1079,7 @@
"event_cancelled": "Deze gebeurtenis is geannuleerd",
"emailed_information_about_cancelled_event": "We hebben u en de andere deelnemers een e-mail gestuurd om hen te informeren.",
"this_input_will_shown_booking_this_event": "Deze invoer wordt getoond tijdens het boeken van dit evenement",
"meeting_url_in_conformation_email": "De vergader-URL wordt vermeld in de bevestigings-e-mail",
"meeting_url_in_confirmation_email": "De vergader-URL wordt vermeld in de bevestigings-e-mail",
"url_start_with_https": "URL moet beginnen met http:// of https://",
"number_provided": "Telefoonnummer wordt verstrekt",
"before_event_trigger": "voordat de gebeurtenis begint",
@@ -941,7 +941,7 @@
"reschedule_placeholder": "Fortell andre hvorfor du må endre tidspunktet",
"emailed_information_about_cancelled_event": "Vi sendte deg og de andre deltakerne en e-post for å informere.",
"this_input_will_shown_booking_this_event": "Dette inputfeltet vil vises når man booker denne hendelsen",
"meeting_url_in_conformation_email": "Møte-url ligger i e-postbekreftelsen",
"meeting_url_in_confirmation_email": "Møte-url ligger i e-postbekreftelsen",
"url_start_with_https": "URL må begynne med http:// eller https://",
"number_provided": "Telefonnummer vil bli oppgitt",
"before_event_trigger": "før hendelsen starter",
@@ -1075,7 +1075,7 @@
"event_cancelled": "To wydarzenie zostało anulowane",
"emailed_information_about_cancelled_event": "Wysłaliśmy wiadomość e-mail Tobie i innym uczestnikom, aby ich o tym poinformować.",
"this_input_will_shown_booking_this_event": "To wejście zostanie wyświetlone podczas rezerwacji tego wydarzenia",
"meeting_url_in_conformation_email": "Adres URL spotkania znajduje się w wiadomości e-mail z potwierdzeniem",
"meeting_url_in_confirmation_email": "Adres URL spotkania znajduje się w wiadomości e-mail z potwierdzeniem",
"url_start_with_https": "Adres URL musi zaczynać się od http:// lub https://",
"number_provided": "Numer telefonu zostanie podany.",
"before_event_trigger": "przed rozpoczęciem wydarzenia",
@@ -1091,7 +1091,7 @@
"event_cancelled": "Este evento foi cancelado",
"emailed_information_about_cancelled_event": "Avisamos por e-mail aos outros participantes e a você.",
"this_input_will_shown_booking_this_event": "Este campo será mostrado na reserva deste evento",
"meeting_url_in_conformation_email": "O URL da reunião está no e-mail de confirmação",
"meeting_url_in_confirmation_email": "O URL da reunião está no e-mail de confirmação",
"url_start_with_https": "O URL precisa começar com http:// ou https://",
"number_provided": "O número de telefone será fornecido",
"before_event_trigger": "antes do início do evento",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Este evento foi cancelado",
"emailed_information_about_cancelled_event": "Enviámos um email com o aviso para si e para os restantes participantes.",
"this_input_will_shown_booking_this_event": "Este campo será mostrado na reserva deste evento",
"meeting_url_in_conformation_email": "O URL da reunião está no email de confirmação",
"meeting_url_in_confirmation_email": "O URL da reunião está no email de confirmação",
"url_start_with_https": "O URL tem de começar por http:// ou https://",
"number_provided": "Será fornecido um número de telefone",
"before_event_trigger": "antes do evento iniciar",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Acest eveniment este anulat",
"emailed_information_about_cancelled_event": "V-am trimis un e-mail de informare dvs. și celorlalți participanți.",
"this_input_will_shown_booking_this_event": "Datele introduse vor fi afișate la rezervarea acestui eveniment",
"meeting_url_in_conformation_email": "Adresa URL a întâlnirii este în e-mailul de confirmare",
"meeting_url_in_confirmation_email": "Adresa URL a întâlnirii este în e-mailul de confirmare",
"url_start_with_https": "Adresa URL trebuie să înceapă cu http:// or https://",
"number_provided": "Numărul de telefon va fi furnizat",
"before_event_trigger": "înainte de începerea evenimentului",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Это событие отменено",
"emailed_information_about_cancelled_event": "Мы сообщили об этом вам и другим участникам по электронной почте.",
"this_input_will_shown_booking_this_event": "Этот вопрос будет показан при бронировании события по этому шаблону",
"meeting_url_in_conformation_email": "Ссылка на встречу находится в письме с подтверждением",
"meeting_url_in_confirmation_email": "Ссылка на встречу находится в письме с подтверждением",
"url_start_with_https": "URL-адрес должен начинаться с http:// или https://",
"number_provided": "Будет указан номер телефона",
"before_event_trigger": "до начала события",
@@ -1076,7 +1076,7 @@
"event_cancelled": "Ovaj događaj je otkazan",
"emailed_information_about_cancelled_event": "Poslali smo imejl vama i drugim polaznicima kao obaveštenje.",
"this_input_will_shown_booking_this_event": "Ovaj unos će biti prikazan kad se rezerviše ovaj događaj",
"meeting_url_in_conformation_email": "Url za sastanak je u imejlu za potvrdu",
"meeting_url_in_confirmation_email": "Url za sastanak je u imejlu za potvrdu",
"url_start_with_https": "Potrebno je URL da počinje sa http:// or https://",
"number_provided": "Broj telefona će biti dostavljen",
"before_event_trigger": "pre početka događaja",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Den här händelsen är inställd",
"emailed_information_about_cancelled_event": "Vi mejlade dig och de andra deltagarna för att informera om detta.",
"this_input_will_shown_booking_this_event": "Detta visas vid bokning av denna händelse",
"meeting_url_in_conformation_email": "Mötets webbadress finns i bekräftelsemejlet",
"meeting_url_in_confirmation_email": "Mötets webbadress finns i bekräftelsemejlet",
"url_start_with_https": "Webbadress behöver inledas med http:// eller https://",
"number_provided": "Telefonnummer kommer att anges",
"before_event_trigger": "innan händelsen startar",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Bu etkinlik iptal edildi",
"emailed_information_about_cancelled_event": "Sizi ve diğer katılımcıları bilgilendirmek için bir e-posta gönderdik.",
"this_input_will_shown_booking_this_event": "Bu girdi, bu etkinlik için rezervasyon yapılırken gösterilecek",
"meeting_url_in_conformation_email": "Toplantı URL'si onay e-postasında mevcut",
"meeting_url_in_confirmation_email": "Toplantı URL'si onay e-postasında mevcut",
"url_start_with_https": "URL'nin http:// veya https:// ile başlaması gerekiyor",
"number_provided": "Telefon numarası sağlanacaktır",
"before_event_trigger": "etkinlik başlamadan önce",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Цей захід скасовано",
"emailed_information_about_cancelled_event": "Ми сповістили вас та інших учасників електронною поштою.",
"this_input_will_shown_booking_this_event": "Це поле введення буде показано під час бронювання часу для цього заходу",
"meeting_url_in_conformation_email": "URL-адресу наради вказано в листі з підтвердженням",
"meeting_url_in_confirmation_email": "URL-адресу наради вказано в листі з підтвердженням",
"url_start_with_https": "URL-адреса має починатися з http:// або https://",
"number_provided": "Номер телефону буде надано",
"before_event_trigger": "до початку заходу",
@@ -1075,7 +1075,7 @@
"event_cancelled": "Sự kiện này bị huỷ",
"emailed_information_about_cancelled_event": "Chúng tôi đã gửi email cho bạn và những người tham dự khác để cho họ biết.",
"this_input_will_shown_booking_this_event": "Trường này sẽ được hiển thị khi đặt sự kiện này",
"meeting_url_in_conformation_email": "Url cuộc họp nằm trong email xác nhận",
"meeting_url_in_confirmation_email": "Url cuộc họp nằm trong email xác nhận",
"url_start_with_https": "URL cần bắt đầu bằng http:// hoặc https://",
"number_provided": "Số điện thoại sẽ được cung ứng",
"before_event_trigger": "trước khi sự kiện bắt đầu",
@@ -1079,7 +1079,7 @@
"event_cancelled": "此活动已取消",
"emailed_information_about_cancelled_event": "我们已向您和其他参与者发送电子邮件来通知他们。",
"this_input_will_shown_booking_this_event": "在预约此活动时将显示该输入框",
"meeting_url_in_conformation_email": "会议链接含在确认电子邮件中",
"meeting_url_in_confirmation_email": "会议链接含在确认电子邮件中",
"url_start_with_https": "链接需要以 http:// 或 https:// 开头",
"number_provided": "将提供电话号码",
"before_event_trigger": "活动开始前",
@@ -1945,5 +1945,6 @@
"insights_subtitle": "查看您活动的预约 insights",
"custom_plan": "自定义计划",
"include_calendar_event": "包括日历活动",
"recently_added": "最近添加",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ 在此上方添加您的新字符串 ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
}
@@ -1075,7 +1075,7 @@
"event_cancelled": "此活動已取消",
"emailed_information_about_cancelled_event": "我們已給您和其他與會者發送通知電子郵件。",
"this_input_will_shown_booking_this_event": "輸入欄會在預約此活動的時候顯示",
"meeting_url_in_conformation_email": "會議網址請參見確認電子郵件",
"meeting_url_in_confirmation_email": "會議網址請參見確認電子郵件",
"url_start_with_https": "URL 需要以 http:// 或 https:// 開頭",
"number_provided": "將提供電話號碼",
"before_event_trigger": "活動開始前",
+7 -9
View File
@@ -207,16 +207,14 @@ html.todesktop nav a svg{
https://github.com/tailwindlabs/tailwindcss/pull/5732
*/
@layer utilities {
@layer responsive {
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
/* Chrome, Safari and Opera */
.no-scrollbar::-webkit-scrollbar {
display: none;
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
.no-scrollbar {
-ms-overflow-style: none; /* IE and Edge */
scrollbar-width: none; /* Firefox */
}
}
+6 -1
View File
@@ -31,7 +31,7 @@ beforeAll(async () => {
describe("next.config.js - Org Rewrite", () => {
const orgHostRegExp = (subdomainRegExp: string) =>
// RegExp copied from pagesAndRewritePaths.js orgHostPath. Do make the change there as well.
new RegExp(`^(?<orgSlug>${subdomainRegExp})\\..*`);
new RegExp(`^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`);
describe("Host matching based on NEXT_PUBLIC_WEBAPP_URL", () => {
it("https://app.cal.com", () => {
@@ -87,6 +87,11 @@ describe("next.config.js - Org Rewrite", () => {
?.orgSlug
).toEqual("some-other");
});
it("Should ignore Vercel preview URLs", () => {
const subdomainRegExp = getSubdomainRegExp("https://cal-xxxxxxxx-cal.vercel.app");
expect(orgHostRegExp(subdomainRegExp).exec("https://cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null')
expect(orgHostRegExp(subdomainRegExp).exec("cal-xxxxxxxx-cal.vercel.app")).toMatchInlineSnapshot('null')
});
});
describe("Rewrite", () => {
-8
View File
@@ -1,8 +0,0 @@
{
"builds": [
{
"src": "package.json",
"use": "https://profile-next-cold-starts.vercel.app/builder.tgz"
}
]
}
+3 -2
View File
@@ -34,7 +34,7 @@ export async function getAppWithMetadata(app: { dirName: string } | { slug: stri
export async function getAppRegistry() {
const dbApps = await prisma.app.findMany({
where: { enabled: true },
select: { dirName: true, slug: true, categories: true, enabled: true },
select: { dirName: true, slug: true, categories: true, enabled: true, createdAt: true },
});
const apps = [] as App[];
const installCountPerApp = await getInstallCountPerApp();
@@ -44,7 +44,7 @@ export async function getAppRegistry() {
// Skip if app isn't installed
/* This is now handled from the DB */
// if (!app.installed) return apps;
app.createdAt = dbapp.createdAt.toISOString();
apps.push({
...app,
category: app.category || "other",
@@ -101,6 +101,7 @@ export async function getAppRegistryWithCredentials(userId: number, userAdminTea
// Skip if app isn't installed
/* This is now handled from the DB */
// if (!app.installed) return apps;
app.createdAt = dbapp.createdAt.toISOString();
let dependencyData: {
name?: string;
installed?: boolean;
@@ -2,6 +2,7 @@ import type { GetStaticPropsContext } from "next";
export const AppSetupPageMap = {
zapier: import("../../zapier/pages/setup/_getStaticProps"),
make: import("../../make/pages/setup/_getStaticProps"),
};
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
@@ -9,6 +9,7 @@ export const AppSetupMap = {
"exchange2016-calendar": dynamic(() => import("../../exchange2016calendar/pages/setup")),
"caldav-calendar": dynamic(() => import("../../caldavcalendar/pages/setup")),
zapier: dynamic(() => import("../../zapier/pages/setup")),
make: dynamic(() => import("../../make/pages/setup")),
closecom: dynamic(() => import("../../closecom/pages/setup")),
sendgrid: dynamic(() => import("../../sendgrid/pages/setup")),
paypal: dynamic(() => import("../../paypal/pages/setup")),
@@ -12,6 +12,7 @@ import { appKeysSchema as gtm_zod_ts } from "./gtm/zod";
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
import { appKeysSchema as make_zod_ts } from "./make/zod";
import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod";
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
@@ -43,6 +44,7 @@ export const appKeysSchemas = {
hubspot: hubspot_zod_ts,
jitsivideo: jitsivideo_zod_ts,
larkcalendar: larkcalendar_zod_ts,
make: make_zod_ts,
metapixel: metapixel_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,
@@ -29,6 +29,7 @@ import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
import { metadata as huddle01video__metadata_ts } from "./huddle01video/_metadata";
import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata";
import make_config_json from "./make/config.json";
import metapixel_config_json from "./metapixel/config.json";
import mirotalk_config_json from "./mirotalk/config.json";
import n8n_config_json from "./n8n/config.json";
@@ -99,6 +100,7 @@ export const appStoreMetadata = {
huddle01video: huddle01video__metadata_ts,
jitsivideo: jitsivideo__metadata_ts,
larkcalendar: larkcalendar__metadata_ts,
make: make_config_json,
metapixel: metapixel_config_json,
mirotalk: mirotalk_config_json,
n8n: n8n_config_json,
@@ -12,6 +12,7 @@ import { appDataSchema as gtm_zod_ts } from "./gtm/zod";
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
import { appDataSchema as jitsivideo_zod_ts } from "./jitsivideo/zod";
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
import { appDataSchema as make_zod_ts } from "./make/zod";
import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod";
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
@@ -43,6 +44,7 @@ export const appDataSchemas = {
hubspot: hubspot_zod_ts,
jitsivideo: jitsivideo_zod_ts,
larkcalendar: larkcalendar_zod_ts,
make: make_zod_ts,
metapixel: metapixel_zod_ts,
office365calendar: office365calendar_zod_ts,
office365video: office365video_zod_ts,
@@ -29,6 +29,7 @@ export const apiHandlers = {
huddle01video: import("./huddle01video/api"),
jitsivideo: import("./jitsivideo/api"),
larkcalendar: import("./larkcalendar/api"),
make: import("./make/api"),
metapixel: import("./metapixel/api"),
mirotalk: import("./mirotalk/api"),
n8n: import("./n8n/api"),
@@ -97,10 +97,9 @@ export default class GoogleCalendarService implements Calendar {
responseStatus: "accepted",
})) || [];
return new Promise(async (resolve, reject) => {
const [mainHostDestinationCalendar] =
calEventRaw?.destinationCalendar && calEventRaw?.destinationCalendar.length > 0
? calEventRaw.destinationCalendar
: [];
const selectedHostDestinationCalendar = calEventRaw.destinationCalendar?.find(
(cal) => cal.credentialId === credentialId
);
const myGoogleAuth = await this.auth.getToken();
const payload: calendar_v3.Schema$Event = {
summary: calEventRaw.title,
@@ -119,8 +118,8 @@ export default class GoogleCalendarService implements Calendar {
id: String(calEventRaw.organizer.id),
responseStatus: "accepted",
organizer: true,
email: mainHostDestinationCalendar?.externalId
? mainHostDestinationCalendar.externalId
email: selectedHostDestinationCalendar?.externalId
? selectedHostDestinationCalendar.externalId
: calEventRaw.organizer.email,
},
...eventAttendees,
@@ -144,14 +143,14 @@ export default class GoogleCalendarService implements Calendar {
});
// Find in calEventRaw.destinationCalendar the one with the same credentialId
const selectedCalendar = calEventRaw.destinationCalendar?.find(
(cal) => cal.credentialId === credentialId
)?.externalId;
const selectedCalendar =
calEventRaw.destinationCalendar?.find((cal) => cal.credentialId === credentialId)?.externalId ||
"primary";
calendar.events.insert(
{
auth: myGoogleAuth,
calendarId: selectedCalendar || "primary",
calendarId: selectedCalendar,
requestBody: payload,
conferenceDataVersion: 1,
sendUpdates: "none",
+1
View File
@@ -26,6 +26,7 @@ const appStore = {
webexvideo: () => import("./webex"),
giphy: () => import("./giphy"),
zapier: () => import("./zapier"),
make: () => import("./make"),
exchange2013calendar: () => import("./exchange2013calendar"),
exchange2016calendar: () => import("./exchange2016calendar"),
exchangecalendar: () => import("./exchangecalendar"),
+1 -1
View File
@@ -403,7 +403,7 @@ export function getSuccessPageLocationMessage(
locationToDisplay == t("web_conference");
} else if (isConfirmed) {
locationToDisplay =
getHumanReadableLocationValue(location, t) + ": " + t("meeting_url_in_conformation_email");
getHumanReadableLocationValue(location, t) + ": " + t("meeting_url_in_confirmation_email");
} else {
locationToDisplay = t("web_conferencing_details_to_follow");
}
+10
View File
@@ -0,0 +1,10 @@
---
items:
- 1.jpeg
- 2.jpeg
- 3.jpeg
- 4.jpeg
- 5.jpeg
---
Workflow automation for everyone. Use the Cal.com app in Make to automate your workflows when a booking is created, rescheduled, cancelled or when a meeting has ended. You can also get all your booking with the 'List Bookings' module.<br /><br />**After Installation:** Have you lost your API key? You can always generate a new key on the <a href="/apps/make/setup">**<ins>Make Setup Page</ins>**</a>
+22
View File
@@ -0,0 +1,22 @@
# Setting up Make Integration
1. Install the app from the Cal app store and generate an API key. Copy the API key.
2. Go to `/admin/apps/automation` in Cal and set the `invite_link` for Make to `https://www.make.com/en/hq/app-invitation/6cb2772b61966508dd8f414ba3b44510` to use the app.
3. Create a [Make account](https://www.make.com/en/login), if you don't have one.
4. Go to `Scenarios` in the sidebar and click on **Create a new scenario**.
5. Search for `Cal.com` in the apps list and select from the list of triggers - Booking Created, Booking Deleted, Booking Rescheduled, Meeting Ended
6. To create a **connection** you will need your Cal deployment url and the app API Key generated above. You only need to create a **connection** once, all webhooks can use that connection.
7. Setup the webhook for the desired event in Make.
8. To delete a webhook, go to `Webhooks` in the left sidebar in Make, pick the webhook you want to delete and click **delete**.
## Localhost or Self-hosting
Localhost urls can not be used as the base URL for api endpoints
Possible solution: using [https://ngrok.com/](https://ngrok.com/)
1. Create Account
2. [Download](https://ngrok.com/download) ngrok and start a tunnel to your running localhost
- Use forwarding url as your baseUrl for the URL endpoints
3. Use the ngrok url as your Cal deployment url when creating the **Connection** in Make.
+20
View File
@@ -0,0 +1,20 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
redirect: {
newTab: true,
url: "/apps/make/setup",
},
createCredential: ({ appType, user, slug, teamId }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {}, teamId }),
};
export default handler;
+5
View File
@@ -0,0 +1,5 @@
export { default as add } from "./add";
export { default as listBookings } from "./subscriptions/listBookings";
export { default as deleteSubscription } from "./subscriptions/deleteSubscription";
export { default as addSubscription } from "./subscriptions/addSubscription";
export { default as me } from "./subscriptions/me";
@@ -0,0 +1,38 @@
import type { NextApiRequest, NextApiResponse } from "next";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { addSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
if (!apiKey) {
return res.status(401).json({ message: "No API key provided" });
}
const validKey = await findValidApiKey(apiKey, "make");
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
const { subscriberUrl, triggerEvent } = req.body;
const createAppSubscription = await addSubscription({
appApiKey: validKey,
triggerEvent: triggerEvent,
subscriberUrl: subscriberUrl,
appId: "make",
});
if (!createAppSubscription) {
return res.status(500).json({ message: "Could not create subscription." });
}
res.status(200).json(createAppSubscription);
}
export default defaultHandler({
POST: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -0,0 +1,40 @@
import type { NextApiRequest, NextApiResponse } from "next";
import z from "zod";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { deleteSubscription } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
const querySchema = z.object({
apiKey: z.string(),
id: z.string(),
});
async function handler(req: NextApiRequest, res: NextApiResponse) {
const { apiKey, id } = querySchema.parse(req.query);
if (!apiKey) {
return res.status(401).json({ message: "No API key provided" });
}
const validKey = await findValidApiKey(apiKey, "make");
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
const deleteEventSubscription = await deleteSubscription({
appApiKey: validKey,
webhookId: id,
appId: "make",
});
if (!deleteEventSubscription) {
return res.status(500).json({ message: "Could not delete subscription." });
}
res.status(204).json({ message: "Subscription is deleted." });
}
export default defaultHandler({
DELETE: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from "next";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import { listBookings } from "@calcom/features/webhooks/lib/scheduleTrigger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
if (!apiKey) {
return res.status(401).json({ message: "No API key provided" });
}
const validKey = await findValidApiKey(apiKey, "make");
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
const bookings = await listBookings(validKey);
if (!bookings) {
return res.status(500).json({ message: "Unable to get bookings." });
}
if (bookings.length === 0) {
const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
return res.status(404).json({
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
});
}
res.status(201).json(bookings);
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
@@ -0,0 +1,35 @@
import type { NextApiRequest, NextApiResponse } from "next";
import findValidApiKey from "@calcom/features/ee/api-keys/lib/findValidApiKey";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const apiKey = req.query.apiKey as string;
if (!apiKey) {
return res.status(401).json({ message: "No API key provided" });
}
const validKey = await findValidApiKey(apiKey, "make");
if (!validKey) {
return res.status(401).json({ message: "API key not valid" });
}
if (req.method === "GET") {
try {
const user = await prisma.user.findFirst({
where: {
id: validKey.userId,
},
select: {
username: true,
},
});
res.status(201).json(user);
} catch (error) {
console.error(error);
return res.status(500).json({ message: "Unable to get User." });
}
}
}
+18
View File
@@ -0,0 +1,18 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Make",
"slug": "make",
"type": "make_automation",
"logo": "icon.svg",
"url": "https://cal.com/apps/make",
"variant": "automation",
"categories": ["automation"],
"publisher": "aa2dee2",
"email": "support@cal.com",
"description": "Automate workflows",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "basic",
"imageSrc": "icon.svg",
"dirName": "make"
}
+1
View File
@@ -0,0 +1 @@
export * as api from "./api";
+15
View File
@@ -0,0 +1,15 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/make",
"version": "0.0.0",
"main": "./index.ts",
"description": "Workflow automation for everyone. Use the Cal.com Make app to trigger your workflows when a booking is created, rescheduled, or cancelled, or after a meeting ends.",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*",
"@types/node-schedule": "^2.1.0"
}
}
@@ -0,0 +1,20 @@
import type { GetStaticPropsContext } from "next";
import getAppKeysFromSlug from "../../../_utils/getAppKeysFromSlug";
export interface IMakeSetupProps {
inviteLink: string;
}
export const getStaticProps = async (ctx: GetStaticPropsContext) => {
if (typeof ctx.params?.slug !== "string") return { notFound: true } as const;
let inviteLink = "";
const appKeys = await getAppKeysFromSlug("make");
if (typeof appKeys.invite_link === "string") inviteLink = appKeys.invite_link;
return {
props: {
inviteLink,
},
};
};
@@ -0,0 +1,178 @@
import type { InferGetStaticPropsType } from "next";
import { Trans } from "next-i18next";
import Link from "next/link";
import { useState } from "react";
import { Toaster } from "react-hot-toast";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, Tooltip, showToast } from "@calcom/ui";
import { Clipboard } from "@calcom/ui/components/icon";
import type { getStaticProps } from "./_getStaticProps";
const MAKE = "make";
export default function MakeSetup({ inviteLink }: InferGetStaticPropsType<typeof getStaticProps>) {
const [newApiKeys, setNewApiKeys] = useState<Record<string, string>>({});
const { t } = useLocale();
const utils = trpc.useContext();
const integrations = trpc.viewer.integrations.useQuery({ variant: "automation" });
const oldApiKey = trpc.viewer.apiKeys.findKeyOfType.useQuery({ appId: MAKE });
const teamsList = trpc.viewer.teams.listOwnedTeams.useQuery(undefined, {
refetchOnWindowFocus: false,
});
const teams = teamsList.data?.map((team) => ({ id: team.id, name: team.name }));
const deleteApiKey = trpc.viewer.apiKeys.delete.useMutation({
onSuccess: () => {
utils.viewer.apiKeys.findKeyOfType.invalidate();
},
});
const makeCredentials: { userCredentialIds: number[] } | undefined = integrations.data?.items.find(
(item: { type: string }) => item.type === "make_automation"
);
const [credentialId] = makeCredentials?.userCredentialIds || [false];
const showContent = integrations.data && integrations.isSuccess && credentialId;
async function createApiKey(teamId?: number) {
const event = { note: "Make", expiresAt: null, appId: MAKE, teamId };
const apiKey = await utils.client.viewer.apiKeys.create.mutate(event);
if (oldApiKey.data) {
const oldKey = teamId
? oldApiKey.data.find((key) => key.teamId === teamId)
: oldApiKey.data.find((key) => !key.teamId);
if (oldKey) {
deleteApiKey.mutate({
id: oldKey.id,
});
}
}
return apiKey;
}
async function generateApiKey(teamId?: number) {
const apiKey = await createApiKey(teamId);
setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey });
}
if (integrations.isLoading) {
return <div className="bg-emphasis absolute z-50 flex h-screen w-full items-center" />;
}
return (
<div className="bg-emphasis flex h-screen">
{showContent ? (
<div className="bg-default m-auto max-w-[43em] overflow-auto rounded pb-10 md:p-10">
<div className="md:flex md:flex-row">
<div className="invisible md:visible">
<img className="h-11" src="/api/app-store/make/icon.svg" alt="Make Logo" />
</div>
<div className="ml-2 ltr:mr-2 rtl:ml-2 md:ml-5">
<div className="text-default">{t("setting_up_make")}</div>
<>
<div className="mt-1 text-xl">{t("generate_api_key")}:</div>
{!teams ? (
<Button color="secondary" onClick={() => createApiKey()} className="mb-4 mt-2">
{t("generate_api_key")}
</Button>
) : (
<>
<div className="mt-8 text-sm font-semibold">Your event types:</div>
{!newApiKeys[""] ? (
<Button color="secondary" onClick={() => generateApiKey()} className="mb-4 mt-2">
{t("generate_api_key")}
</Button>
) : (
<CopyApiKey apiKey={newApiKeys[""]} />
)}
{teams.map((team) => {
return (
<div key={team.name}>
<div className="mt-2 text-sm font-semibold">{team.name}:</div>
{!newApiKeys[team.id] ? (
<Button
color="secondary"
onClick={() => generateApiKey(team.id)}
className="mb-4 mt-2">
{t("generate_api_key")}
</Button>
) : (
<CopyApiKey apiKey={newApiKeys[team.id]} />
)}
</div>
);
})}
</>
)}
</>
<ol className="mb-5 ml-5 mt-5 list-decimal ltr:mr-5 rtl:ml-5">
<Trans i18nKey="invite_link_make">
<li>
Go to
<a href={inviteLink} className="ml-1 mr-1 text-orange-600 underline">
Make Invite Link
</a>
and install the Cal.com app.
</li>
</Trans>
<Trans i18nKey="make_setup_instructions">
<li>Log into your Make account and create a new Scenario.</li>
<li>Select Cal.com as your Trigger app. Also choose a Trigger event.</li>
<li>Choose your account and then enter your Unique API Key.</li>
<li>Test your Trigger.</li>
<li>You&apos;re set!</li>
</Trans>
</ol>
<Link href="/apps/installed/automation?hl=make" passHref={true} legacyBehavior>
<Button color="secondary">{t("done")}</Button>
</Link>
</div>
</div>
</div>
) : (
<div className="ml-5 mt-5">
<div>{t("install_make_app")}</div>
<div className="mt-3">
<Link href="/apps/make" passHref={true} legacyBehavior>
<Button>{t("go_to_app_store")}</Button>
</Link>
</div>
</div>
)}
<Toaster position="bottom-right" />
</div>
);
}
const CopyApiKey = ({ apiKey }: { apiKey: string }) => {
const { t } = useLocale();
return (
<div>
<div className="my-2 mt-3 flex-wrap sm:flex sm:flex-nowrap">
<code className="bg-subtle h-full w-full whitespace-pre-wrap rounded-md py-[6px] pl-2 pr-2 sm:rounded-r-none sm:pr-5">
{apiKey}
</code>
<Tooltip side="top" content={t("copy_to_clipboard")}>
<Button
onClick={() => {
navigator.clipboard.writeText(apiKey);
showToast(t("api_key_copied"), "success");
}}
type="button"
className="mt-4 text-base sm:mt-0 sm:rounded-l-none">
<Clipboard className="h-4 w-4 ltr:mr-2 rtl:ml-2" />
{t("copy")}
</Button>
</Tooltip>
</div>
<div className="text-subtle mb-5 mt-2 text-sm">{t("copy_somewhere_safe")}</div>
</div>
);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512">
<g id="Make-App-Icon-Circle" transform="translate(3757 -1767)">
<circle id="Ellipse_10" data-name="Ellipse 10" cx="256" cy="256" r="256" transform="translate(-3757 1767)" fill="#6d00cc"/>
<path id="Path_141560" data-name="Path 141560" d="M244.78,14.544a7.187,7.187,0,0,0-7.186,7.192V213.927a7.19,7.19,0,0,0,7.186,7.192h52.063a7.187,7.187,0,0,0,7.186-7.192V21.736a7.183,7.183,0,0,0-7.186-7.192ZM92.066,17.083,5.77,188.795a7.191,7.191,0,0,0,3.192,9.654l46.514,23.379a7.184,7.184,0,0,0,9.654-3.2l86.3-171.711a7.184,7.184,0,0,0-3.2-9.654L101.719,13.886a7.2,7.2,0,0,0-9.654,3.2m72.592.614L127.731,204.876a7.189,7.189,0,0,0,5.632,8.442l51.028,10.306a7.2,7.2,0,0,0,8.481-5.665L229.8,30.786a7.19,7.19,0,0,0-5.637-8.442L173.133,12.038a7.391,7.391,0,0,0-1.427-.144,7.194,7.194,0,0,0-7.048,5.8" transform="translate(-3676.356 1905.425)" fill="#fff"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 952 B

+7
View File
@@ -0,0 +1,7 @@
import { z } from "zod";
export const appDataSchema = z.object({});
export const appKeysSchema = z.object({
invite_link: z.string().min(1),
});
-7
View File
@@ -4,7 +4,6 @@ import z from "zod";
import Paypal from "@calcom/app-store/paypal/lib/Paypal";
import { findPaymentCredentials } from "@calcom/features/ee/payments/api/paypal-webhook";
import { IS_PRODUCTION } from "@calcom/lib/constants";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
@@ -78,12 +77,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
}
return;
} catch (_err) {
const err = getErrorFromUnknown(_err);
res.status(200).send({
message: err.message,
stack: IS_PRODUCTION ? undefined : err.stack,
});
res.redirect(`/booking/${req.query.bookingUid}?paypalPaymentStatus=failed`);
}
}
+1 -1
View File
@@ -173,7 +173,7 @@ class Paypal {
}
} catch (error) {
console.error(error);
return false;
throw error;
}
return false;
}
@@ -41,25 +41,6 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
}}
/>
</div>
<TextField
name="Plausible URL"
defaultValue="https://plausible.io/js/script.js"
placeholder="https://plausible.io/js/script.js"
value={plausibleUrl}
disabled={disabled}
onChange={(e) => {
setAppData("PLAUSIBLE_URL", e.target.value);
}}
/>
<TextField
disabled={disabled}
name="Tracked Domain"
placeholder="yourdomain.com"
value={trackingId}
onChange={(e) => {
setAppData("trackingId", e.target.value);
}}
/>
</AppCard>
);
};
@@ -5,6 +5,13 @@ import type { Fixtures } from "@calcom/web/playwright/lib/fixtures";
import { test } from "@calcom/web/playwright/lib/fixtures";
import { gotoRoutingLink } from "@calcom/web/playwright/lib/testUtils";
import {
addForm,
saveCurrentForm,
verifySelectOptions,
addOneFieldAndDescriptionAndSaveForm,
} from "./testUtils";
function todo(title: string) {
// eslint-disable-next-line playwright/no-skipped-test, @typescript-eslint/no-empty-function
test.skip(title, () => {});
@@ -407,22 +414,6 @@ async function fillSeededForm(page: Page, routingFormId: string) {
await expect(page.locator("text=Custom Page Result")).toBeVisible();
}
export async function addForm(page: Page, { name = "Test Form Name" } = {}) {
await page.goto("/routing-forms/forms");
await page.click('[data-testid="new-routing-form"]');
// Choose to create the Form for the user(which is the first option) and not the team
await page.click('[data-testid="option-0"]');
await page.fill("input[name]", name);
await page.click('[data-testid="add-form"]');
await page.waitForSelector('[data-testid="add-field"]');
const url = page.url();
const formId = new URL(url).pathname.split("/").at(-1);
if (!formId) {
throw new Error("Form ID couldn't be determined from url");
}
return formId;
}
async function addAllTypesOfFieldsAndSaveForm(
formId: string,
page: Page,
@@ -480,46 +471,6 @@ async function addAllTypesOfFieldsAndSaveForm(
};
}
export async function addOneFieldAndDescriptionAndSaveForm(
formId: string,
page: Page,
form: { description?: string; field?: { typeIndex: number; label: string } }
) {
await page.goto(`apps/routing-forms/form-edit/${formId}`);
await page.click('[data-testid="add-field"]');
if (form.description) {
await page.fill('[data-testid="description"]', form.description);
}
// Verify all Options of SelectBox
const { optionsInUi: types } = await verifySelectOptions(
{ selector: ".data-testid-field-type", nth: 0 },
["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"],
page
);
const nextFieldIndex = (await page.locator('[data-testid="field"]').count()) - 1;
if (form.field) {
await page.fill(`[data-testid="fields.${nextFieldIndex}.label"]`, form.field.label);
await page
.locator('[data-testid="field"]')
.nth(nextFieldIndex)
.locator(".data-testid-field-type")
.click();
await page
.locator('[data-testid="field"]')
.nth(nextFieldIndex)
.locator('[id*="react-select-"][aria-disabled]')
.nth(form.field.typeIndex)
.click();
}
await saveCurrentForm(page);
return {
types,
};
}
async function selectOption({
page,
selector,
@@ -551,26 +502,6 @@ async function verifyFieldOptionsInRule(options: string[], page: Page) {
);
}
async function verifySelectOptions(
selector: { selector: string; nth: number },
expectedOptions: string[],
page: Page
) {
await page.locator(selector.selector).nth(selector.nth).click();
const selectOptions = await page
.locator(selector.selector)
.nth(selector.nth)
.locator('[id*="react-select-"][aria-disabled]')
.allInnerTexts();
const sortedSelectOptions = [...selectOptions].sort();
const sortedExpectedOptions = [...expectedOptions].sort();
expect(sortedSelectOptions).toEqual(sortedExpectedOptions);
return {
optionsInUi: selectOptions,
};
}
async function selectNewRoute(page: Page, { routeSelectNumber = 1 } = {}) {
await selectOption({
selector: {
@@ -581,8 +512,3 @@ async function selectNewRoute(page: Page, { routeSelectNumber = 1 } = {}) {
page,
});
}
async function saveCurrentForm(page: Page) {
await page.click('[data-testid="update-form"]');
await page.waitForSelector(".data-testid-toast-success");
}
@@ -0,0 +1,83 @@
import type { Page } from "@playwright/test";
import { expect } from "@playwright/test";
export async function addForm(page: Page, { name = "Test Form Name" } = {}) {
await page.goto("/routing-forms/forms");
await page.click('[data-testid="new-routing-form"]');
// Choose to create the Form for the user(which is the first option) and not the team
await page.click('[data-testid="option-0"]');
await page.fill("input[name]", name);
await page.click('[data-testid="add-form"]');
await page.waitForSelector('[data-testid="add-field"]');
const url = page.url();
const formId = new URL(url).pathname.split("/").at(-1);
if (!formId) {
throw new Error("Form ID couldn't be determined from url");
}
return formId;
}
export async function addOneFieldAndDescriptionAndSaveForm(
formId: string,
page: Page,
form: { description?: string; field?: { typeIndex: number; label: string } }
) {
await page.goto(`apps/routing-forms/form-edit/${formId}`);
await page.click('[data-testid="add-field"]');
if (form.description) {
await page.fill('[data-testid="description"]', form.description);
}
// Verify all Options of SelectBox
const { optionsInUi: types } = await verifySelectOptions(
{ selector: ".data-testid-field-type", nth: 0 },
["Email", "Long Text", "Multiple Selection", "Number", "Phone", "Single Selection", "Short Text"],
page
);
const nextFieldIndex = (await page.locator('[data-testid="field"]').count()) - 1;
if (form.field) {
await page.fill(`[data-testid="fields.${nextFieldIndex}.label"]`, form.field.label);
await page
.locator('[data-testid="field"]')
.nth(nextFieldIndex)
.locator(".data-testid-field-type")
.click();
await page
.locator('[data-testid="field"]')
.nth(nextFieldIndex)
.locator('[id*="react-select-"][aria-disabled]')
.nth(form.field.typeIndex)
.click();
}
await saveCurrentForm(page);
return {
types,
};
}
export async function saveCurrentForm(page: Page) {
await page.click('[data-testid="update-form"]');
await page.waitForSelector(".data-testid-toast-success");
}
export async function verifySelectOptions(
selector: { selector: string; nth: number },
expectedOptions: string[],
page: Page
) {
await page.locator(selector.selector).nth(selector.nth).click();
const selectOptions = await page
.locator(selector.selector)
.nth(selector.nth)
.locator('[id*="react-select-"][aria-disabled]')
.allInnerTexts();
const sortedSelectOptions = [...selectOptions].sort();
const sortedExpectedOptions = [...expectedOptions].sort();
expect(sortedSelectOptions).toEqual(sortedExpectedOptions);
return {
optionsInUi: selectOptions,
};
}
+36 -11
View File
@@ -12,32 +12,50 @@ export async function onFormSubmission(
form: Ensure<SerializableForm<App_RoutingForms_Form> & { user: User }, "fields">,
response: Response
) {
const fieldResponsesByName: Record<string, (typeof response)[keyof typeof response]["value"]> = {};
const fieldResponsesByName: Record<
string,
{
value: Response[keyof Response]["value"];
}
> = {};
for (const [fieldId, fieldResponse] of Object.entries(response)) {
// Use the label lowercased as the key to identify a field.
const key =
form.fields.find((f) => f.id === fieldId)?.identifier ||
(fieldResponse.label as keyof typeof fieldResponsesByName);
fieldResponsesByName[key] = fieldResponse.value;
fieldResponsesByName[key] = {
value: fieldResponse.value,
};
}
const subscriberOptions = {
userId: form.user.id,
triggerEvent: WebhookTriggerEvents.FORM_SUBMITTED,
// When team routing forms are implemented, we need to make sure to add the teamId here
teamId: null,
...getWebhookTargetEntity(form),
};
const webhooks = await getWebhooks(subscriberOptions);
const promises = webhooks.map((webhook) => {
sendGenericWebhookPayload(
webhook.secret,
"FORM_SUBMITTED",
new Date().toISOString(),
sendGenericWebhookPayload({
secretKey: webhook.secret,
triggerEvent: "FORM_SUBMITTED",
createdAt: new Date().toISOString(),
webhook,
fieldResponsesByName
).catch((e) => {
data: {
formId: form.id,
formName: form.name,
teamId: form.teamId,
responses: fieldResponsesByName,
},
rootData: {
// Send responses unwrapped at root level for backwards compatibility
...Object.entries(fieldResponsesByName).reduce((acc, [key, value]) => {
acc[key] = value.value;
return acc;
}, {} as Record<string, Response[keyof Response]["value"]>),
},
}).catch((e) => {
console.error(`Error executing routing form webhook`, webhook, e);
});
});
@@ -66,3 +84,10 @@ export const sendResponseEmail = async (
logger.error("Error sending response email", e);
}
};
function getWebhookTargetEntity(form: { teamId?: number | null; user: { id: number } }) {
// If it's a team form, the target must be team webhook
// If it's a user form, the target must be user webhook
const isTeamForm = form.teamId;
return { userId: isTeamForm ? null : form.user.id, teamId: isTeamForm ? form.teamId : null };
}
@@ -4,7 +4,7 @@ import { expect } from "@playwright/test";
import {
addForm as addRoutingForm,
addOneFieldAndDescriptionAndSaveForm,
} from "@calcom/app-store/routing-forms/playwright/tests/basic.e2e";
} from "@calcom/app-store/routing-forms/playwright/tests/testUtils";
import { CAL_URL } from "@calcom/lib/constants";
import type { Fixtures } from "@calcom/web/playwright/lib/fixtures";
import { test } from "@calcom/web/playwright/lib/fixtures";

Some files were not shown because too many files have changed in this diff Show More