refactor: Use template literal instead of '+' operator (#11444)
Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: zomars <zomars@me.com>pull/11647/head^2
parent
a186b130cb
commit
3e08c66888
|
@ -94,11 +94,13 @@ To develop locally:
|
||||||
|
|
||||||
6. Setup Node
|
6. Setup Node
|
||||||
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nvm use
|
nvm use
|
||||||
```
|
```
|
||||||
|
|
||||||
You first might need to install the specific version and then use it:
|
You first might need to install the specific version and then use it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nvm install && nvm use
|
nvm install && nvm use
|
||||||
```
|
```
|
||||||
|
@ -134,6 +136,7 @@ yarn test-e2e
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Resolving issues
|
#### Resolving issues
|
||||||
|
|
||||||
##### E2E test browsers not installed
|
##### E2E test browsers not installed
|
||||||
|
|
||||||
Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`:
|
Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`:
|
||||||
|
|
|
@ -144,17 +144,20 @@ Here is what you need to be able to run Cal.com.
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Set up your `.env` file
|
4. Set up your `.env` file
|
||||||
|
|
||||||
- Duplicate `.env.example` to `.env`
|
- Duplicate `.env.example` to `.env`
|
||||||
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
|
||||||
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
|
||||||
|
|
||||||
5. Setup Node
|
5. Setup Node
|
||||||
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
If your Node version does not meet the project's requirements as instructed by the docs, "nvm" (Node Version Manager) allows using Node at the version required by the project:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nvm use
|
nvm use
|
||||||
```
|
```
|
||||||
|
|
||||||
You first might need to install the specific version and then use it:
|
You first might need to install the specific version and then use it:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
nvm install && nvm use
|
nvm install && nvm use
|
||||||
```
|
```
|
||||||
|
@ -234,6 +237,7 @@ echo 'NEXT_PUBLIC_DEBUG=1' >> .env
|
||||||
```
|
```
|
||||||
|
|
||||||
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
|
1. Run [mailhog](https://github.com/mailhog/MailHog) to view emails sent during development
|
||||||
|
|
||||||
> **_NOTE:_** Required when `E2E_TEST_MAILHOG_ENABLED` is "1"
|
> **_NOTE:_** Required when `E2E_TEST_MAILHOG_ENABLED` is "1"
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
@ -273,6 +277,7 @@ yarn playwright show-report test-results/reports/playwright-html-report
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Resolving issues
|
#### Resolving issues
|
||||||
|
|
||||||
##### E2E test browsers not installed
|
##### E2E test browsers not installed
|
||||||
|
|
||||||
Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`:
|
Run `npx playwright install` to download test browsers and resolve the error below when running `yarn test-e2e`:
|
||||||
|
@ -493,8 +498,7 @@ following
|
||||||
5. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/basecamp3/callback` replacing Cal.com URL with the URI at which your application runs.
|
5. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/basecamp3/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||||
6. Click on done and copy the Client ID and secret into the `BASECAMP3_CLIENT_ID` and `BASECAMP3_CLIENT_SECRET` fields.
|
6. Click on done and copy the Client ID and secret into the `BASECAMP3_CLIENT_ID` and `BASECAMP3_CLIENT_SECRET` fields.
|
||||||
7. Set the `BASECAMP3_CLIENT_SECRET` env variable to `{your_domain} ({support_email})`.
|
7. Set the `BASECAMP3_CLIENT_SECRET` env variable to `{your_domain} ({support_email})`.
|
||||||
For example, `Cal.com (support@cal.com)`.
|
For example, `Cal.com (support@cal.com)`.
|
||||||
|
|
||||||
|
|
||||||
### Obtaining HubSpot Client ID and Secret
|
### Obtaining HubSpot Client ID and Secret
|
||||||
|
|
||||||
|
@ -529,6 +533,7 @@ For example, `Cal.com (support@cal.com)`.
|
||||||
### Obtaining Zoho Calendar Client ID and Secret
|
### Obtaining Zoho Calendar Client ID and Secret
|
||||||
|
|
||||||
[Follow these steps](./packages/app-store/zohocalendar/)
|
[Follow these steps](./packages/app-store/zohocalendar/)
|
||||||
|
|
||||||
### Obtaining Zoho Bigin Client ID and Secret
|
### Obtaining Zoho Bigin Client ID and Secret
|
||||||
|
|
||||||
[Follow these steps](./packages/app-store/zoho-bigin/)
|
[Follow these steps](./packages/app-store/zoho-bigin/)
|
||||||
|
|
|
@ -4,9 +4,9 @@ Welcome to the first stage of Cal.ai!
|
||||||
|
|
||||||
This app lets you chat with your calendar via email:
|
This app lets you chat with your calendar via email:
|
||||||
|
|
||||||
- Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?"
|
- Turn informal emails into bookings eg. forward "wanna meet tmrw at 2pm?"
|
||||||
- List and rearrange your bookings eg. "Cancel my next meeting"
|
- List and rearrange your bookings eg. "Cancel my next meeting"
|
||||||
- Answer basic questions about your busiest times eg. "How does my Tuesday look?"
|
- Answer basic questions about your busiest times eg. "How does my Tuesday look?"
|
||||||
|
|
||||||
The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings.
|
The core logic is contained in [agent/route.ts](/apps/ai/src/app/api/agent/route.ts). Here, a [LangChain Agent Executor](https://docs.langchain.com/docs/components/agents/agent-executor) is tasked with following your instructions. Given your last-known timezone, working hours, and busy times, it attempts to CRUD your bookings.
|
||||||
|
|
||||||
|
@ -24,10 +24,10 @@ If you haven't yet, please run the [root setup](/README.md) steps.
|
||||||
|
|
||||||
Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. You'll need:
|
Before running the app, please see [env.mjs](./src/env.mjs) for all required environment variables. You'll need:
|
||||||
|
|
||||||
- An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4
|
- An [OpenAI API key](https://platform.openai.com/account/api-keys) with access to GPT-4
|
||||||
- A [SendGrid API key](https://app.sendgrid.com/settings/api_keys)
|
- A [SendGrid API key](https://app.sendgrid.com/settings/api_keys)
|
||||||
- A default sender email (for example, `ai@cal.dev`)
|
- A default sender email (for example, `ai@cal.dev`)
|
||||||
- The Cal.ai's app ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts))
|
- The Cal.ai's app ID and URL (see [add.ts](/packages/app-store/cal-ai/api/index.ts))
|
||||||
|
|
||||||
To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`.
|
To stand up the API and AI apps simultaneously, simply run `yarn dev:ai`.
|
||||||
|
|
||||||
|
|
|
@ -74,18 +74,19 @@ ${
|
||||||
? `The email references the following @usernames and emails: ${users
|
? `The email references the following @usernames and emails: ${users
|
||||||
.map(
|
.map(
|
||||||
(u) =>
|
(u) =>
|
||||||
(u.id ? `, id: ${u.id}` : "id: (non user)") +
|
`${
|
||||||
(u.username
|
(u.id ? `, id: ${u.id}` : "id: (non user)") +
|
||||||
? u.type === "fromUsername"
|
(u.username
|
||||||
? `, username: @${u.username}`
|
? u.type === "fromUsername"
|
||||||
: ", username: REDACTED"
|
? `, username: @${u.username}`
|
||||||
: ", (no username)") +
|
: ", username: REDACTED"
|
||||||
(u.email
|
: ", (no username)") +
|
||||||
? u.type === "fromEmail"
|
(u.email
|
||||||
? `, email: ${u.email}`
|
? u.type === "fromEmail"
|
||||||
: ", email: REDACTED"
|
? `, email: ${u.email}`
|
||||||
: ", (no email)") +
|
: ", email: REDACTED"
|
||||||
";"
|
: ", (no email)")
|
||||||
|
};`
|
||||||
)
|
)
|
||||||
.join("\n")}`
|
.join("\n")}`
|
||||||
: ""
|
: ""
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from "express";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import { createMocks } from "node-mocks-http";
|
import { createMocks } from "node-mocks-http";
|
||||||
|
@ -8,7 +10,6 @@ import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
||||||
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
import { buildBooking, buildEventType, buildWebhook } from "@calcom/lib/test/builder";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import prismaMock from "../../../../../tests/libs/__mocks__/prisma";
|
|
||||||
import handler from "../../../pages/api/bookings/_post";
|
import handler from "../../../pages/api/bookings/_post";
|
||||||
|
|
||||||
type CustomNextApiRequest = NextApiRequest & Request;
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
|
|
@ -265,8 +265,8 @@ export const AppPage = ({
|
||||||
|
|
||||||
{price !== 0 && (
|
{price !== 0 && (
|
||||||
<span className="block text-right">
|
<span className="block text-right">
|
||||||
{feeType === "usage-based" ? commission + "% + " + priceInDollar + "/booking" : priceInDollar}
|
{feeType === "usage-based" ? `${commission}% + ${priceInDollar}/booking` : priceInDollar}
|
||||||
{feeType === "monthly" && "/" + t("month")}
|
{feeType === "monthly" && `/${t("month")}`}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -286,7 +286,7 @@ export const AppPage = ({
|
||||||
currency: "USD",
|
currency: "USD",
|
||||||
useGrouping: false,
|
useGrouping: false,
|
||||||
}).format(price)}
|
}).format(price)}
|
||||||
{feeType === "monthly" && "/" + t("month")}
|
{feeType === "monthly" && `/${t("month")}`}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
|
@ -323,7 +323,7 @@ export const AppPage = ({
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={"mailto:" + email}>
|
href={`mailto:${email}`}>
|
||||||
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
<Mail className="text-subtle -mt-px mr-1 inline h-4 w-4" />
|
||||||
|
|
||||||
{email}
|
{email}
|
||||||
|
|
|
@ -130,7 +130,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||||
title={t("something_went_wrong")}
|
title={t("something_went_wrong")}
|
||||||
message={
|
message={
|
||||||
<span>
|
<span>
|
||||||
<Link href={"/apps/" + item.integration.slug}>{item.integration.name}</Link>:{" "}
|
<Link href={`/apps/${item.integration.slug}`}>{item.integration.name}</Link>:{" "}
|
||||||
{t("calendar_error")}
|
{t("calendar_error")}
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
@ -380,7 +380,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className={"w-full px-4" + (isRejected ? " line-through" : "")}>
|
<td className={`w-full px-4${isRejected ? " line-through" : ""}`}>
|
||||||
<Link href={bookingLink}>
|
<Link href={bookingLink}>
|
||||||
{/* Time and Badges for mobile */}
|
{/* Time and Badges for mobile */}
|
||||||
<div className="w-full pb-2 pt-4 sm:hidden">
|
<div className="w-full pb-2 pt-4 sm:hidden">
|
||||||
|
@ -576,7 +576,7 @@ const FirstAttendee = ({
|
||||||
<a
|
<a
|
||||||
key={user.email}
|
key={user.email}
|
||||||
className=" hover:text-blue-500"
|
className=" hover:text-blue-500"
|
||||||
href={"mailto:" + user.email}
|
href={`mailto:${user.email}`}
|
||||||
onClick={(e) => e.stopPropagation()}>
|
onClick={(e) => e.stopPropagation()}>
|
||||||
{user.name}
|
{user.name}
|
||||||
</a>
|
</a>
|
||||||
|
@ -590,7 +590,7 @@ type AttendeeProps = {
|
||||||
|
|
||||||
const Attendee = ({ email, name }: AttendeeProps) => {
|
const Attendee = ({ email, name }: AttendeeProps) => {
|
||||||
return (
|
return (
|
||||||
<a className="hover:text-blue-500" href={"mailto:" + email} onClick={(e) => e.stopPropagation()}>
|
<a className="hover:text-blue-500" href={`mailto:${email}`} onClick={(e) => e.stopPropagation()}>
|
||||||
{name || email}
|
{name || email}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
@ -121,7 +121,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: `Invalid URL for ${eventLocationType.label}. ${
|
message: `Invalid URL for ${eventLocationType.label}. ${
|
||||||
sampleUrl ? "Sample URL: " + sampleUrl : ""
|
sampleUrl ? `Sample URL: ${sampleUrl}` : ""
|
||||||
}`,
|
}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -67,7 +67,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
||||||
|
|
||||||
eventType.bookingFields.forEach(({ name }) => {
|
eventType.bookingFields.forEach(({ name }) => {
|
||||||
bookingFields[name] = name + " input";
|
bookingFields[name] = `${name} input`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventNameObject: EventNameObjectType = {
|
const eventNameObject: EventNameObjectType = {
|
||||||
|
|
|
@ -189,7 +189,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||||
label: minutes + " " + t("minutes"),
|
label: `minutes ${t("minutes")}`,
|
||||||
value: minutes,
|
value: minutes,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
@ -225,7 +225,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||||
value: 0,
|
value: 0,
|
||||||
},
|
},
|
||||||
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
...[5, 10, 15, 20, 30, 45, 60, 90, 120].map((minutes) => ({
|
||||||
label: minutes + " " + t("minutes"),
|
label: `minutes ${t("minutes")}`,
|
||||||
value: minutes,
|
value: minutes,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
@ -272,7 +272,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||||
value: -1,
|
value: -1,
|
||||||
},
|
},
|
||||||
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
|
...[5, 10, 15, 20, 30, 45, 60, 75, 90, 105, 120].map((minutes) => ({
|
||||||
label: minutes + " " + t("minutes"),
|
label: `minutes ${t("minutes")}`,
|
||||||
value: minutes,
|
value: minutes,
|
||||||
})),
|
})),
|
||||||
];
|
];
|
||||||
|
|
|
@ -247,7 +247,7 @@ function EventTypeSingleLayout({
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
backPath="/event-types"
|
backPath="/event-types"
|
||||||
title={eventType.title + " | " + t("event_type")}
|
title={`${eventType.title} | ${t("event_type")}`}
|
||||||
heading={eventType.title}
|
heading={eventType.title}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
|
|
|
@ -22,7 +22,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||||
return (
|
return (
|
||||||
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
|
<Link key={member.id} href={{ pathname: `/${member.username}`, query: queryParamsToForward }}>
|
||||||
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
<div className="sm:min-w-80 sm:max-w-80 bg-default hover:bg-muted border-subtle group flex min-h-full flex-col space-y-2 rounded-md border p-4 hover:cursor-pointer">
|
||||||
<Avatar size="md" alt={member.name || ""} imageSrc={"/" + member.username + "/avatar.png"} />
|
<Avatar size="md" alt={member.name || ""} imageSrc={`/${member.username}/avatar.png`} />
|
||||||
<section className="mt-2 line-clamp-4 w-full space-y-1">
|
<section className="mt-2 line-clamp-4 w-full space-y-1">
|
||||||
<p className="text-default font-medium">{member.name}</p>
|
<p className="text-default font-medium">{member.name}</p>
|
||||||
<div className="text-subtle line-clamp-3 overflow-ellipsis text-sm font-normal">
|
<div className="text-subtle line-clamp-3 overflow-ellipsis text-sm font-normal">
|
||||||
|
|
|
@ -179,14 +179,14 @@ function getThemeProviderProps({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const appearanceIdSuffix = themeBasis ? ":" + themeBasis : "";
|
const appearanceIdSuffix = themeBasis ? `:${themeBasis}` : "";
|
||||||
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
|
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
|
||||||
let embedExplicitlySetThemeSuffix = "";
|
let embedExplicitlySetThemeSuffix = "";
|
||||||
|
|
||||||
if (typeof window !== "undefined") {
|
if (typeof window !== "undefined") {
|
||||||
const embedTheme = window.getEmbedTheme();
|
const embedTheme = window.getEmbedTheme();
|
||||||
if (embedTheme) {
|
if (embedTheme) {
|
||||||
embedExplicitlySetThemeSuffix = ":" + embedTheme;
|
embedExplicitlySetThemeSuffix = `:${embedTheme}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,7 @@ function getCspPolicy(nonce: string) {
|
||||||
script-src ${
|
script-src ${
|
||||||
IS_PRODUCTION
|
IS_PRODUCTION
|
||||||
? // 'self' 'unsafe-inline' https: added for Browsers not supporting strict-dynamic not supporting strict-dynamic
|
? // 'self' 'unsafe-inline' https: added for Browsers not supporting strict-dynamic not supporting strict-dynamic
|
||||||
"'nonce-" + nonce + "' 'strict-dynamic' 'self' 'unsafe-inline' https:"
|
`'nonce-${nonce}' 'strict-dynamic' 'self' 'unsafe-inline' https:`
|
||||||
: // Note: We could use 'strict-dynamic' with 'nonce-..' instead of unsafe-inline but there are some streaming related scripts that get blocked(because they don't have nonce on them). It causes a really frustrating full page error model by Next.js to show up sometimes
|
: // Note: We could use 'strict-dynamic' with 'nonce-..' instead of unsafe-inline but there are some streaming related scripts that get blocked(because they don't have nonce on them). It causes a really frustrating full page error model by Next.js to show up sometimes
|
||||||
"'unsafe-inline' 'unsafe-eval' https: http:"
|
"'unsafe-inline' 'unsafe-eval' https: http:"
|
||||||
};
|
};
|
||||||
|
|
|
@ -15,14 +15,9 @@ export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
||||||
const destinationUrlObj = new URL(ssrResponse.redirect.destination, "https://base");
|
const destinationUrlObj = new URL(ssrResponse.redirect.destination, "https://base");
|
||||||
|
|
||||||
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
|
// Make sure that redirect happens to /embed page and pass on embed query param as is for preserving Cal JS API namespace
|
||||||
const newDestinationUrl =
|
const newDestinationUrl = `${
|
||||||
destinationUrlObj.pathname +
|
destinationUrlObj.pathname
|
||||||
"/embed?" +
|
}/embed?${destinationUrlObj.searchParams.toString()}&layout=${layout}&embed=${embed}`;
|
||||||
destinationUrlObj.searchParams.toString() +
|
|
||||||
"&layout=" +
|
|
||||||
layout +
|
|
||||||
"&embed=" +
|
|
||||||
embed;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...ssrResponse,
|
...ssrResponse,
|
||||||
|
|
|
@ -21,11 +21,11 @@ process.env.NEXT_PUBLIC_CALCOM_VERSION = version;
|
||||||
|
|
||||||
// So we can test deploy previews preview
|
// So we can test deploy previews preview
|
||||||
if (process.env.VERCEL_URL && !process.env.NEXT_PUBLIC_WEBAPP_URL) {
|
if (process.env.VERCEL_URL && !process.env.NEXT_PUBLIC_WEBAPP_URL) {
|
||||||
process.env.NEXT_PUBLIC_WEBAPP_URL = "https://" + process.env.VERCEL_URL;
|
process.env.NEXT_PUBLIC_WEBAPP_URL = `https://${process.env.VERCEL_URL}`;
|
||||||
}
|
}
|
||||||
// Check for configuration of NEXTAUTH_URL before overriding
|
// Check for configuration of NEXTAUTH_URL before overriding
|
||||||
if (!process.env.NEXTAUTH_URL && process.env.NEXT_PUBLIC_WEBAPP_URL) {
|
if (!process.env.NEXTAUTH_URL && process.env.NEXT_PUBLIC_WEBAPP_URL) {
|
||||||
process.env.NEXTAUTH_URL = process.env.NEXT_PUBLIC_WEBAPP_URL + "/api/auth";
|
process.env.NEXTAUTH_URL = `${process.env.NEXT_PUBLIC_WEBAPP_URL}/api/auth`;
|
||||||
}
|
}
|
||||||
if (!process.env.NEXT_PUBLIC_WEBSITE_URL) {
|
if (!process.env.NEXT_PUBLIC_WEBSITE_URL) {
|
||||||
process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||||
|
|
|
@ -125,7 +125,7 @@ export function UserPage(props: InferGetServerSidePropsType<typeof getServerSide
|
||||||
{user.away ? (
|
{user.away ? (
|
||||||
<div className="overflow-hidden rounded-sm border ">
|
<div className="overflow-hidden rounded-sm border ">
|
||||||
<div className="text-muted p-8 text-center">
|
<div className="text-muted p-8 text-center">
|
||||||
<h2 className="font-cal text-default mb-2 text-3xl">😴{" " + t("user_away")}</h2>
|
<h2 className="font-cal text-default mb-2 text-3xl">😴{` ${t("user_away")}`}</h2>
|
||||||
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
|
<p className="mx-auto max-w-md">{t("user_away_description") as string}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -19,7 +19,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
return res.status(400).json({ message: "Google client_secret missing." });
|
return res.status(400).json({ message: "Google client_secret missing." });
|
||||||
|
|
||||||
// use differnt callback to normal calendar connection
|
// use differnt callback to normal calendar connection
|
||||||
const redirect_uri = WEBAPP_URL + "/api/teams/googleworkspace/callback";
|
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
|
||||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||||
|
|
||||||
const authUrl = oAuth2Client.generateAuthUrl({
|
const authUrl = oAuth2Client.generateAuthUrl({
|
||||||
|
|
|
@ -36,7 +36,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
if (!client_secret || typeof client_secret !== "string")
|
if (!client_secret || typeof client_secret !== "string")
|
||||||
return res.status(400).json({ message: "Google client_secret missing." });
|
return res.status(400).json({ message: "Google client_secret missing." });
|
||||||
|
|
||||||
const redirect_uri = WEBAPP_URL + "/api/teams/googleworkspace/callback";
|
const redirect_uri = `${WEBAPP_URL}/api/teams/googleworkspace/callback`;
|
||||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||||
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
|
@ -54,11 +54,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!teamId) {
|
if (!teamId) {
|
||||||
res.redirect(getSafeRedirectUrl(WEBAPP_URL + "/settings") ?? `${WEBAPP_URL}/teams`);
|
res.redirect(getSafeRedirectUrl(`${WEBAPP_URL}/settings`) ?? `${WEBAPP_URL}/teams`);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.redirect(
|
res.redirect(
|
||||||
getSafeRedirectUrl(WEBAPP_URL + `/settings/teams/${teamId}/members?inviteModal=true&bulk=true`) ??
|
getSafeRedirectUrl(`${WEBAPP_URL}/settings/teams/${teamId}/members?inviteModal=true&bulk=true`) ??
|
||||||
`${WEBAPP_URL}/teams`
|
`${WEBAPP_URL}/teams`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default function Apps({ categories }: inferSSRProps<typeof getServerSideP
|
||||||
{categories.map((category) => (
|
{categories.map((category) => (
|
||||||
<Link
|
<Link
|
||||||
key={category.name}
|
key={category.name}
|
||||||
href={"/apps/categories/" + category.name}
|
href={`/apps/categories/${category.name}`}
|
||||||
data-testid={`app-store-category-${category.name}`}
|
data-testid={`app-store-category-${category.name}`}
|
||||||
className="bg-subtle relative flex rounded-sm px-6 py-4 sm:block">
|
className="bg-subtle relative flex rounded-sm px-6 py-4 sm:block">
|
||||||
<div className="self-center">
|
<div className="self-center">
|
||||||
|
|
|
@ -30,12 +30,12 @@ export default function Provider(props: SSOProviderPageProps) {
|
||||||
const email = searchParams?.get("email");
|
const email = searchParams?.get("email");
|
||||||
|
|
||||||
if (!email) {
|
if (!email) {
|
||||||
router.push("/auth/error?error=" + "Email not provided");
|
router.push(`/auth/error?error=Email not provided`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!props.isSAMLLoginEnabled) {
|
if (!props.isSAMLLoginEnabled) {
|
||||||
router.push("/auth/error?error=" + "SAML login not enabled");
|
router.push(`/auth/error?error=SAML login not enabled`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
const providerParam = asStringOrNull(context.query.provider);
|
const providerParam = asStringOrNull(context.query.provider);
|
||||||
const emailParam = asStringOrNull(context.query.email);
|
const emailParam = asStringOrNull(context.query.email);
|
||||||
const usernameParam = asStringOrNull(context.query.username);
|
const usernameParam = asStringOrNull(context.query.username);
|
||||||
const successDestination = "/getting-started" + (usernameParam ? `?username=${usernameParam}` : "");
|
const successDestination = `/getting-started${usernameParam ? `?username=${usernameParam}` : ""}`;
|
||||||
if (!providerParam) {
|
if (!providerParam) {
|
||||||
throw new Error(`File is not named sso/[provider]`);
|
throw new Error(`File is not named sso/[provider]`);
|
||||||
}
|
}
|
||||||
|
@ -120,7 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
if (error) {
|
if (error) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: "/auth/error?error=" + error,
|
destination: `/auth/error?error=${error}`,
|
||||||
permanent: false,
|
permanent: false,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -120,7 +120,7 @@ export default function Verify() {
|
||||||
? "Your payment failed"
|
? "Your payment failed"
|
||||||
: sessionId
|
: sessionId
|
||||||
? "Payment successful!"
|
? "Payment successful!"
|
||||||
: "Verify your email" + " | " + APP_NAME}
|
: `Verify your email | ${APP_NAME}`}
|
||||||
</title>
|
</title>
|
||||||
</Head>
|
</Head>
|
||||||
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
<div className="flex min-h-screen flex-col items-center justify-center px-6">
|
||||||
|
|
|
@ -151,7 +151,7 @@ export default function Availability() {
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
backPath={fromEventType ? true : "/availability"}
|
backPath={fromEventType ? true : "/availability"}
|
||||||
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")}
|
title={schedule?.name ? `${schedule.name} | ${t("availability")}` : t("availability")}
|
||||||
heading={
|
heading={
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|
|
@ -133,7 +133,7 @@ Troubleshoot.PageWrapper = PageWrapper;
|
||||||
function convertMinsToHrsMins(mins: number) {
|
function convertMinsToHrsMins(mins: number) {
|
||||||
const h = Math.floor(mins / 60);
|
const h = Math.floor(mins / 60);
|
||||||
const m = mins % 60;
|
const m = mins % 60;
|
||||||
const hs = h < 10 ? "0" + h : h;
|
const hs = h < 10 ? `0${h}` : h;
|
||||||
const ms = m < 10 ? "0" + m : m;
|
const ms = m < 10 ? `0${m}` : m;
|
||||||
return `${hs}:${ms}`;
|
return `${hs}:${ms}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -269,13 +269,13 @@ export default function Success(props: SuccessProps) {
|
||||||
}
|
}
|
||||||
if (needsConfirmation) {
|
if (needsConfirmation) {
|
||||||
if (props.profile.name !== null) {
|
if (props.profile.name !== null) {
|
||||||
return t("user_needs_to_confirm_or_reject_booking" + titleSuffix, {
|
return t(`user_needs_to_confirm_or_reject_booking${titleSuffix}`, {
|
||||||
user: props.profile.name,
|
user: props.profile.name,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return t("needs_to_be_confirmed_or_rejected" + titleSuffix);
|
return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
|
||||||
}
|
}
|
||||||
return t("emailed_you_and_attendees" + titleSuffix);
|
return t(`emailed_you_and_attendees${titleSuffix}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
|
// This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
|
||||||
|
@ -592,23 +592,24 @@ export default function Success(props: SuccessProps) {
|
||||||
</span>
|
</span>
|
||||||
<div className="justify-left mt-1 flex text-left sm:mt-0">
|
<div className="justify-left mt-1 flex text-left sm:mt-0">
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={`https://calendar.google.com/calendar/r/eventedit?dates=${date
|
||||||
`https://calendar.google.com/calendar/r/eventedit?dates=${date
|
.utc()
|
||||||
.utc()
|
.format("YYYYMMDDTHHmmss[Z]")}/${date
|
||||||
.format("YYYYMMDDTHHmmss[Z]")}/${date
|
.add(calculatedDuration, "minute")
|
||||||
.add(calculatedDuration, "minute")
|
.utc()
|
||||||
.utc()
|
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
|
||||||
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
|
props.eventType.description
|
||||||
props.eventType.description
|
}${
|
||||||
}` +
|
typeof locationVideoCallUrl === "string"
|
||||||
(typeof locationVideoCallUrl === "string"
|
? `&location=${encodeURIComponent(locationVideoCallUrl)}`
|
||||||
? "&location=" + encodeURIComponent(locationVideoCallUrl)
|
: ""
|
||||||
: "") +
|
}${
|
||||||
(props.eventType.recurringEvent
|
props.eventType.recurringEvent
|
||||||
? "&recur=" +
|
? `&recur=${encodeURIComponent(
|
||||||
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
|
new RRule(props.eventType.recurringEvent).toString()
|
||||||
: "")
|
)}`
|
||||||
}
|
: ""
|
||||||
|
}`}
|
||||||
className="text-default border-subtle h-10 w-10 rounded-sm border px-3 py-2 ltr:mr-2 rtl:ml-2">
|
className="text-default border-subtle h-10 w-10 rounded-sm border px-3 py-2 ltr:mr-2 rtl:ml-2">
|
||||||
<svg
|
<svg
|
||||||
className="-mt-1.5 inline-block h-4 w-4"
|
className="-mt-1.5 inline-block h-4 w-4"
|
||||||
|
@ -622,17 +623,17 @@ export default function Success(props: SuccessProps) {
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
encodeURI(
|
encodeURI(
|
||||||
"https://outlook.live.com/calendar/0/deeplink/compose?body=" +
|
`https://outlook.live.com/calendar/0/deeplink/compose?body=${
|
||||||
props.eventType.description +
|
props.eventType.description
|
||||||
"&enddt=" +
|
}&enddt=${date
|
||||||
date.add(calculatedDuration, "minute").utc().format() +
|
.add(calculatedDuration, "minute")
|
||||||
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
.utc()
|
||||||
date.utc().format() +
|
.format()}&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=${date
|
||||||
"&subject=" +
|
.utc()
|
||||||
eventName
|
.format()}&subject=${eventName}`
|
||||||
) +
|
) +
|
||||||
(locationVideoCallUrl
|
(locationVideoCallUrl
|
||||||
? "&location=" + encodeURIComponent(locationVideoCallUrl)
|
? `&location=${encodeURIComponent(locationVideoCallUrl)}`
|
||||||
: "")
|
: "")
|
||||||
}
|
}
|
||||||
className="border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2"
|
className="border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2"
|
||||||
|
@ -649,17 +650,17 @@ export default function Success(props: SuccessProps) {
|
||||||
<Link
|
<Link
|
||||||
href={
|
href={
|
||||||
encodeURI(
|
encodeURI(
|
||||||
"https://outlook.office.com/calendar/0/deeplink/compose?body=" +
|
`https://outlook.office.com/calendar/0/deeplink/compose?body=${
|
||||||
props.eventType.description +
|
props.eventType.description
|
||||||
"&enddt=" +
|
}&enddt=${date
|
||||||
date.add(calculatedDuration, "minute").utc().format() +
|
.add(calculatedDuration, "minute")
|
||||||
"&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=" +
|
.utc()
|
||||||
date.utc().format() +
|
.format()}&path=%2Fcalendar%2Faction%2Fcompose&rru=addevent&startdt=${date
|
||||||
"&subject=" +
|
.utc()
|
||||||
eventName
|
.format()}&subject=${eventName}`
|
||||||
) +
|
) +
|
||||||
(locationVideoCallUrl
|
(locationVideoCallUrl
|
||||||
? "&location=" + encodeURIComponent(locationVideoCallUrl)
|
? `&location=${encodeURIComponent(locationVideoCallUrl)}`
|
||||||
: "")
|
: "")
|
||||||
}
|
}
|
||||||
className="text-default border-subtle mx-2 h-10 w-10 rounded-sm border px-3 py-2"
|
className="text-default border-subtle mx-2 h-10 w-10 rounded-sm border px-3 py-2"
|
||||||
|
@ -674,9 +675,9 @@ export default function Success(props: SuccessProps) {
|
||||||
</svg>
|
</svg>
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href={"data:text/calendar," + eventLink()}
|
href={`data:text/calendar,${eventLink()}`}
|
||||||
className="border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2"
|
className="border-subtle text-default mx-2 h-10 w-10 rounded-sm border px-3 py-2"
|
||||||
download={props.eventType.title + ".ics"}>
|
download={`${props.eventType.title}.ics`}>
|
||||||
<svg
|
<svg
|
||||||
version="1.1"
|
version="1.1"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
|
|
|
@ -140,13 +140,13 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
className="text-default font-semibold ltr:mr-1 rtl:ml-1"
|
className="text-default font-semibold ltr:mr-1 rtl:ml-1"
|
||||||
data-testid={"event-type-title-" + type.id}>
|
data-testid={`event-type-title-${type.id}`}>
|
||||||
{type.title}
|
{type.title}
|
||||||
</span>
|
</span>
|
||||||
{group.profile.slug ? (
|
{group.profile.slug ? (
|
||||||
<small
|
<small
|
||||||
className="text-subtle hidden font-normal leading-4 sm:inline"
|
className="text-subtle hidden font-normal leading-4 sm:inline"
|
||||||
data-testid={"event-type-slug-" + type.id}>
|
data-testid={`event-type-slug-${type.id}`}>
|
||||||
{`/${
|
{`/${
|
||||||
type.schedulingType !== SchedulingType.MANAGED ? group.profile.slug : t("username_placeholder")
|
type.schedulingType !== SchedulingType.MANAGED ? group.profile.slug : t("username_placeholder")
|
||||||
}/${type.slug}`}
|
}/${type.slug}`}
|
||||||
|
@ -177,13 +177,13 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
className="text-default font-semibold ltr:mr-1 rtl:ml-1"
|
className="text-default font-semibold ltr:mr-1 rtl:ml-1"
|
||||||
data-testid={"event-type-title-" + type.id}>
|
data-testid={`event-type-title-${type.id}`}>
|
||||||
{type.title}
|
{type.title}
|
||||||
</span>
|
</span>
|
||||||
{group.profile.slug ? (
|
{group.profile.slug ? (
|
||||||
<small
|
<small
|
||||||
className="text-subtle hidden font-normal leading-4 sm:inline"
|
className="text-subtle hidden font-normal leading-4 sm:inline"
|
||||||
data-testid={"event-type-slug-" + type.id}>
|
data-testid={`event-type-slug-${type.id}`}>
|
||||||
{`/${group.profile.slug}/${type.slug}`}
|
{`/${group.profile.slug}/${type.slug}`}
|
||||||
</small>
|
</small>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -479,7 +479,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<Dropdown modal={false}>
|
<Dropdown modal={false}>
|
||||||
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
|
<DropdownMenuTrigger asChild data-testid={`event-type-options-${type.id}`}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
|
@ -493,9 +493,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
data-testid={"event-type-edit-" + type.id}
|
data-testid={`event-type-edit-${type.id}`}
|
||||||
StartIcon={Edit2}
|
StartIcon={Edit2}
|
||||||
onClick={() => router.push("/event-types/" + type.id)}>
|
onClick={() => router.push(`/event-types/${type.id}`)}>
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
@ -505,7 +505,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
data-testid={"event-type-duplicate-" + type.id}
|
data-testid={`event-type-duplicate-${type.id}`}
|
||||||
StartIcon={Copy}
|
StartIcon={Copy}
|
||||||
onClick={() => openDuplicateModal(type, group)}>
|
onClick={() => openDuplicateModal(type, group)}>
|
||||||
{t("duplicate")}
|
{t("duplicate")}
|
||||||
|
@ -555,7 +555,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-9 mx-5 flex sm:hidden">
|
<div className="min-w-9 mx-5 flex sm:hidden">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
|
<DropdownMenuTrigger asChild data-testid={`event-type-options-${type.id}`}>
|
||||||
<Button type="button" variant="icon" color="secondary" StartIcon={MoreHorizontal} />
|
<Button type="button" variant="icon" color="secondary" StartIcon={MoreHorizontal} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
|
@ -573,7 +573,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
data-testid={"event-type-duplicate-" + type.id}
|
data-testid={`event-type-duplicate-${type.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(calLink);
|
navigator.clipboard.writeText(calLink);
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
|
@ -588,7 +588,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
{isNativeShare ? (
|
{isNativeShare ? (
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
data-testid={"event-type-duplicate-" + type.id}
|
data-testid={`event-type-duplicate-${type.id}`}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator
|
navigator
|
||||||
.share({
|
.share({
|
||||||
|
@ -608,7 +608,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
{!readOnly && (
|
{!readOnly && (
|
||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={() => router.push("/event-types/" + type.id)}
|
onClick={() => router.push(`/event-types/${type.id}`)}
|
||||||
StartIcon={Edit}
|
StartIcon={Edit}
|
||||||
className="w-full rounded-none">
|
className="w-full rounded-none">
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
|
@ -620,7 +620,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={() => openDuplicateModal(type, group)}
|
onClick={() => openDuplicateModal(type, group)}
|
||||||
StartIcon={Copy}
|
StartIcon={Copy}
|
||||||
data-testid={"event-type-duplicate-" + type.id}>
|
data-testid={`event-type-duplicate-${type.id}`}>
|
||||||
{t("duplicate")}
|
{t("duplicate")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
|
@ -63,14 +63,13 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
|
||||||
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef);
|
||||||
|
|
||||||
const eventPage =
|
const eventPage = `${
|
||||||
(eventType.team
|
eventType.team
|
||||||
? "team/" + eventType.team.slug
|
? `team/${eventType.team.slug}`
|
||||||
: dynamicEventSlugRef
|
: dynamicEventSlugRef
|
||||||
? booking.dynamicGroupSlugRef
|
? booking.dynamicGroupSlugRef
|
||||||
: booking.user?.username || "rick") /* This shouldn't happen */ +
|
: booking.user?.username || "rick" /* This shouldn't happen */
|
||||||
"/" +
|
}/${eventType?.slug}`;
|
||||||
eventType?.slug;
|
|
||||||
const destinationUrl = new URLSearchParams();
|
const destinationUrl = new URLSearchParams();
|
||||||
|
|
||||||
destinationUrl.set("rescheduleUid", seatReferenceUid || bookingId);
|
destinationUrl.set("rescheduleUid", seatReferenceUid || bookingId);
|
||||||
|
|
|
@ -190,7 +190,7 @@ const CalendarsView = () => {
|
||||||
}
|
}
|
||||||
<div className="flex-grow truncate pl-2">
|
<div className="flex-grow truncate pl-2">
|
||||||
<ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse">
|
<ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse">
|
||||||
<Link href={"/apps/" + item.integration.slug}>
|
<Link href={`/apps/${item.integration.slug}`}>
|
||||||
{item.integration.name || item.integration.title}
|
{item.integration.name || item.integration.title}
|
||||||
</Link>
|
</Link>
|
||||||
{data?.destinationCalendar?.credentialId === item.credentialId && (
|
{data?.destinationCalendar?.credentialId === item.credentialId && (
|
||||||
|
|
|
@ -281,7 +281,7 @@ export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/auth/login?callbackUrl=" + `${WEBAPP_URL}/${ctx.query.callbackUrl}`,
|
destination: `/auth/login?callbackUrl=${WEBAPP_URL}/${ctx.query.callbackUrl}`,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -102,7 +102,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
||||||
items={type.users.map((user) => ({
|
items={type.users.map((user) => ({
|
||||||
alt: user.name || "",
|
alt: user.name || "",
|
||||||
title: user.name || "",
|
title: user.name || "",
|
||||||
image: "/" + user.username + "/avatar.png" || "",
|
image: `/${user.username}/avatar.png` || "",
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -160,7 +160,7 @@ function TeamPage({ team, isUnpublished, markdownStrippedBio, isValidOrgDomain }
|
||||||
<div className="space-y-6" data-testid="event-types">
|
<div className="space-y-6" data-testid="event-types">
|
||||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||||
<div className="text-muted p-8 text-center">
|
<div className="text-muted p-8 text-center">
|
||||||
<h2 className="font-cal text-emphasis mb-2 text-3xl">{" " + t("org_no_teams_yet")}</h2>
|
<h2 className="font-cal text-emphasis mb-2 text-3xl">{` ${t("org_no_teams_yet")}`}</h2>
|
||||||
<p className="text-emphasis mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
|
<p className="text-emphasis mx-auto max-w-md">{t("org_no_teams_yet_description")}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -324,7 +324,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||||
...type,
|
...type,
|
||||||
users: type.users.map((user) => ({
|
users: type.users.map((user) => ({
|
||||||
...user,
|
...user,
|
||||||
avatar: "/" + user.username + "/avatar.png",
|
avatar: `/${user.username}/avatar.png`,
|
||||||
})),
|
})),
|
||||||
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
|
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
|
||||||
})) ?? null;
|
})) ?? null;
|
||||||
|
|
|
@ -95,12 +95,12 @@ export default function JoinCall(props: JoinCallPageProps) {
|
||||||
<meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} />
|
<meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content={`${WEBSITE_URL}/video`} />
|
<meta property="og:url" content={`${WEBSITE_URL}/video`} />
|
||||||
<meta property="og:title" content={APP_NAME + " Video"} />
|
<meta property="og:title" content={`${APP_NAME} Video`} />
|
||||||
<meta property="og:description" content={t("quick_video_meeting")} />
|
<meta property="og:description" content={t("quick_video_meeting")} />
|
||||||
<meta property="twitter:image" content={SEO_IMG_OGIMG_VIDEO} />
|
<meta property="twitter:image" content={SEO_IMG_OGIMG_VIDEO} />
|
||||||
<meta property="twitter:card" content="summary_large_image" />
|
<meta property="twitter:card" content="summary_large_image" />
|
||||||
<meta property="twitter:url" content={`${WEBSITE_URL}/video`} />
|
<meta property="twitter:url" content={`${WEBSITE_URL}/video`} />
|
||||||
<meta property="twitter:title" content={APP_NAME + " Video"} />
|
<meta property="twitter:title" content={`${APP_NAME} Video`} />
|
||||||
<meta property="twitter:description" content={t("quick_video_meeting")} />
|
<meta property="twitter:description" content={t("quick_video_meeting")} />
|
||||||
</Head>
|
</Head>
|
||||||
<div style={{ zIndex: 2, position: "relative" }}>
|
<div style={{ zIndex: 2, position: "relative" }}>
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default function MeetingUnavailable(props: inferSSRProps<typeof getServer
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-subtle text-center">
|
<p className="text-subtle text-center">
|
||||||
<Calendar className="-mt-1 mr-1 inline-block h-4 w-4" />
|
<Calendar className="-mt-1 mr-1 inline-block h-4 w-4" />
|
||||||
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
|
{dayjs(props.booking.startTime).format(`${detectBrowserTimeFormat}, dddd DD MMMM YYYY`)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -24,7 +24,7 @@ export default function MeetingNotStarted(props: inferSSRProps<typeof getServerS
|
||||||
<h2 className="mb-2 text-center font-medium">{props.booking.title}</h2>
|
<h2 className="mb-2 text-center font-medium">{props.booking.title}</h2>
|
||||||
<p className="text-subtle text-center">
|
<p className="text-subtle text-center">
|
||||||
<Calendar className="-mt-1 mr-1 inline-block h-4 w-4" />
|
<Calendar className="-mt-1 mr-1 inline-block h-4 w-4" />
|
||||||
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
|
{dayjs(props.booking.startTime).format(`${detectBrowserTimeFormat}, dddd DD MMMM YYYY`)}
|
||||||
</p>
|
</p>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@ const otherNonExistingRoutePrefixes = ["forms", "router", "success", "cancel"];
|
||||||
// book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked
|
// book$ ensures that only /book is excluded from rewrite(which is at the end always) and not /booked
|
||||||
|
|
||||||
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
|
let subdomainRegExp = (exports.subdomainRegExp = getSubdomainRegExp(
|
||||||
process.env.NEXT_PUBLIC_WEBAPP_URL || "https://" + process.env.VERCEL_URL
|
process.env.NEXT_PUBLIC_WEBAPP_URL || `https://${process.env.VERCEL_URL}`
|
||||||
));
|
));
|
||||||
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`;
|
exports.orgHostPath = `^(?<orgSlug>${subdomainRegExp})\\.(?!vercel\.app).*`;
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ export const createPaymentsFixture = (page: Page) => {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: {},
|
data: {},
|
||||||
externalId: "DEMO_PAYMENT_FROM_DB_" + Date.now(),
|
externalId: `DEMO_PAYMENT_FROM_DB_${Date.now()}`,
|
||||||
booking: {
|
booking: {
|
||||||
connect: {
|
connect: {
|
||||||
id: bookingId,
|
id: bookingId,
|
||||||
|
|
|
@ -23,7 +23,7 @@ export const nextServer = async ({ port = 3000 } = { port: 3000 }) => {
|
||||||
process.env.PLAYWRIGHT_TEST_BASE_URL =
|
process.env.PLAYWRIGHT_TEST_BASE_URL =
|
||||||
process.env.NEXT_PUBLIC_WEBAPP_URL =
|
process.env.NEXT_PUBLIC_WEBAPP_URL =
|
||||||
process.env.NEXT_PUBLIC_WEBSITE_URL =
|
process.env.NEXT_PUBLIC_WEBSITE_URL =
|
||||||
"http://localhost:" + port;
|
`http://localhost:${port}`;
|
||||||
const app = next({
|
const app = next({
|
||||||
dev: dev,
|
dev: dev,
|
||||||
port,
|
port,
|
||||||
|
@ -46,7 +46,7 @@ export const nextServer = async ({ port = 3000 } = { port: 3000 }) => {
|
||||||
resolve(server);
|
resolve(server);
|
||||||
});
|
});
|
||||||
server.on("error", (error) => {
|
server.on("error", (error) => {
|
||||||
if (error) throw new Error("Could not start Next.js server -" + error.message);
|
if (error) throw new Error(`Could not start Next.js server - ${error.message}`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return server;
|
return server;
|
||||||
|
|
|
@ -65,7 +65,7 @@ test.describe("OAuth Provider", () => {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: "Bearer " + tokenData.access_token,
|
Authorization: `Bearer ${tokenData.access_token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ test.describe("OAuth Provider", () => {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: "Bearer " + tokenData.access_token,
|
Authorization: `Bearer ${tokenData.access_token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -150,7 +150,7 @@ test.describe("OAuth Provider", () => {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: "Bearer " + tokenData.access_token,
|
Authorization: `Bearer ${tokenData.access_token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -181,7 +181,7 @@ test.describe("OAuth Provider", () => {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
Authorization: "Bearer " + tokenData.access_token,
|
Authorization: `Bearer ${tokenData.access_token}`,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -404,8 +404,8 @@ export const getDate = (
|
||||||
year = year + 1;
|
year = year + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const date = _date < 10 ? "0" + _date : _date;
|
const date = _date < 10 ? `0${_date}` : _date;
|
||||||
const month = _month < 10 ? "0" + _month : _month;
|
const month = _month < 10 ? `0${_month}` : _month;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
date,
|
date,
|
||||||
|
|
|
@ -208,7 +208,7 @@ export const AppForm = ({
|
||||||
</Text>
|
</Text>
|
||||||
<Text>
|
<Text>
|
||||||
Tip : Go and change the logo of your {isTemplate ? "template" : "app"} by replacing{" "}
|
Tip : Go and change the logo of your {isTemplate ? "template" : "app"} by replacing{" "}
|
||||||
{getAppDirPath(slug, isTemplate) + "/static/icon.svg"}
|
{`${getAppDirPath(slug, isTemplate)}/static/icon.svg`}
|
||||||
</Text>
|
</Text>
|
||||||
<Newline />
|
<Newline />
|
||||||
<Text bold underline color="blue">
|
<Text bold underline color="blue">
|
||||||
|
|
|
@ -12,7 +12,7 @@ export function Message({
|
||||||
if (message.showInProgressIndicator) {
|
if (message.showInProgressIndicator) {
|
||||||
const interval = setInterval(() => {
|
const interval = setInterval(() => {
|
||||||
setProgressText((progressText) => {
|
setProgressText((progressText) => {
|
||||||
return progressText.length > 3 ? "" : progressText + ".";
|
return progressText.length > 3 ? "" : `${progressText}.`;
|
||||||
});
|
});
|
||||||
}, 1000);
|
}, 1000);
|
||||||
return () => {
|
return () => {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import { APP_STORE_PATH, TEMPLATES_PATH, IS_WINDOWS_PLATFORM} from "./constants";
|
import { APP_STORE_PATH, TEMPLATES_PATH, IS_WINDOWS_PLATFORM } from "./constants";
|
||||||
import execSync from "./utils/execSync";
|
import execSync from "./utils/execSync";
|
||||||
|
|
||||||
const slugify = (str: string) => {
|
const slugify = (str: string) => {
|
||||||
|
@ -70,7 +70,11 @@ export const BaseAppFork = {
|
||||||
const appDirPath = getAppDirPath(slug, isTemplate);
|
const appDirPath = getAppDirPath(slug, isTemplate);
|
||||||
if (!editMode) {
|
if (!editMode) {
|
||||||
await execSync(IS_WINDOWS_PLATFORM ? `mkdir ${appDirPath}` : `mkdir -p ${appDirPath}`);
|
await execSync(IS_WINDOWS_PLATFORM ? `mkdir ${appDirPath}` : `mkdir -p ${appDirPath}`);
|
||||||
await execSync(IS_WINDOWS_PLATFORM ? `xcopy "${TEMPLATES_PATH}\\${template}\\*" "${appDirPath}" /e /i` : `cp -r ${TEMPLATES_PATH}/${template}/* ${appDirPath}`);
|
await execSync(
|
||||||
|
IS_WINDOWS_PLATFORM
|
||||||
|
? `xcopy "${TEMPLATES_PATH}\\${template}\\*" "${appDirPath}" /e /i`
|
||||||
|
: `cp -r ${TEMPLATES_PATH}/${template}/* ${appDirPath}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
if (!oldSlug) {
|
if (!oldSlug) {
|
||||||
throw new Error("oldSlug is required when editMode is true");
|
throw new Error("oldSlug is required when editMode is true");
|
||||||
|
@ -79,7 +83,9 @@ export const BaseAppFork = {
|
||||||
// We need to rename only if they are different
|
// We need to rename only if they are different
|
||||||
const oldAppDirPath = getAppDirPath(oldSlug, isTemplate);
|
const oldAppDirPath = getAppDirPath(oldSlug, isTemplate);
|
||||||
|
|
||||||
await execSync(IS_WINDOWS_PLATFORM ? `move ${oldAppDirPath} ${appDirPath}` : `mv ${oldAppDirPath} ${appDirPath}`);
|
await execSync(
|
||||||
|
IS_WINDOWS_PLATFORM ? `move ${oldAppDirPath} ${appDirPath}` : `mv ${oldAppDirPath} ${appDirPath}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updatePackageJson({ slug, appDirPath, appDescription: description });
|
updatePackageJson({ slug, appDirPath, appDescription: description });
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
## App Contribution Guidelines
|
## App Contribution Guidelines
|
||||||
|
|
||||||
#### `DESCRIPTION.md`
|
#### `DESCRIPTION.md`
|
||||||
|
|
||||||
1. images - include atleast 4 images (do we have a recommended size here?). Can show app in use and/or installation steps
|
1. images - include atleast 4 images (do we have a recommended size here?). Can show app in use and/or installation steps
|
||||||
2. add only file name for images, path not required. i.e. `1.jpeg`, not `/app-store/zohocalendar/1.jpeg`
|
2. add only file name for images, path not required. i.e. `1.jpeg`, not `/app-store/zohocalendar/1.jpeg`
|
||||||
3. description should include what the integration with Cal allows the user to do e.g. `Allows you to sync Cal bookings with your Zoho Calendar`
|
3. description should include what the integration with Cal allows the user to do e.g. `Allows you to sync Cal bookings with your Zoho Calendar`
|
||||||
|
|
||||||
#### `README.md`
|
#### `README.md`
|
||||||
|
|
||||||
1. Include installation instructions and links to the app's website.
|
1. Include installation instructions and links to the app's website.
|
||||||
|
|
||||||
2. For url use `<baseUrl>/api/integrations`, rather than `<Cal.com>/api/integrations`
|
2. For url use `<baseUrl>/api/integrations`, rather than `<Cal.com>/api/integrations`
|
||||||
|
@ -16,6 +18,7 @@
|
||||||
2. description here should not exceed 10 words (this is arbitrary, but should not be long otherwise it's truncated in the app store)
|
2. description here should not exceed 10 words (this is arbitrary, but should not be long otherwise it's truncated in the app store)
|
||||||
|
|
||||||
#### Others
|
#### Others
|
||||||
|
|
||||||
1. Add API documentation links in comments for files `api`, `lib` and `types`
|
1. Add API documentation links in comments for files `api`, `lib` and `types`
|
||||||
2. Use [`AppDeclarativeHandler`](../types/AppHandler.d.ts) across all apps. Whatever isn't supported in it, support that.
|
2. Use [`AppDeclarativeHandler`](../types/AppHandler.d.ts) across all apps. Whatever isn't supported in it, support that.
|
||||||
3. README should be added in the respective app and can be linked in main README [like this](https://github.com/calcom/cal.com/pull/10429/files/155ac84537d12026f595551fe3542e810b029714#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R509)
|
3. README should be added in the respective app and can be linked in main README [like this](https://github.com/calcom/cal.com/pull/10429/files/155ac84537d12026f595551fe3542e810b029714#diff-b335630551682c19a781afebcf4d07bf978fb1f8ac04c6bf87428ed5106870f5R509)
|
||||||
|
|
|
@ -44,7 +44,7 @@ export default function AppCard({
|
||||||
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0">
|
<div className="flex w-full flex-col gap-2 sm:flex-row sm:gap-0">
|
||||||
{/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */}
|
{/* Don't know why but w-[42px] isn't working, started happening when I started using next/dynamic */}
|
||||||
<Link
|
<Link
|
||||||
href={"/apps/" + app.slug}
|
href={`/apps/${app.slug}`}
|
||||||
className={classNames(app?.isInstalled ? "mr-[11px]" : "mr-3", "h-auto w-10 rounded-sm")}>
|
className={classNames(app?.isInstalled ? "mr-[11px]" : "mr-3", "h-auto w-10 rounded-sm")}>
|
||||||
<img
|
<img
|
||||||
className={classNames(
|
className={classNames(
|
||||||
|
|
|
@ -64,7 +64,7 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta
|
||||||
const stateStr = encodeURIComponent(JSON.stringify(state));
|
const stateStr = encodeURIComponent(JSON.stringify(state));
|
||||||
const searchParams = `?state=${stateStr}${teamId ? `&teamId=${teamId}` : ""}`;
|
const searchParams = `?state=${stateStr}${teamId ? `&teamId=${teamId}` : ""}`;
|
||||||
|
|
||||||
const res = await fetch(`/api/integrations/${type}/add` + searchParams);
|
const res = await fetch(`/api/integrations/${type}/add${searchParams}`);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorBody = await res.json();
|
const errorBody = await res.json();
|
||||||
|
|
|
@ -24,9 +24,7 @@ async function handler(req: NextApiRequest) {
|
||||||
client_id,
|
client_id,
|
||||||
};
|
};
|
||||||
const query = stringify(params);
|
const query = stringify(params);
|
||||||
const url = `https://launchpad.37signals.com/authorization/new?${query}&redirect_uri=${
|
const url = `https://launchpad.37signals.com/authorization/new?${query}&redirect_uri=${WEBAPP_URL}/api/integrations/basecamp3/callback`;
|
||||||
WEBAPP_URL + "/api/integrations/basecamp3/callback"
|
|
||||||
}`;
|
|
||||||
return { url };
|
return { url };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -11,7 +11,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const { code } = req.query;
|
const { code } = req.query;
|
||||||
const { client_id, client_secret, user_agent } = await getAppKeysFromSlug("basecamp3");
|
const { client_id, client_secret, user_agent } = await getAppKeysFromSlug("basecamp3");
|
||||||
|
|
||||||
const redirectUri = WEBAPP_URL + "/api/integrations/basecamp3/callback";
|
const redirectUri = `${WEBAPP_URL}/api/integrations/basecamp3/callback`;
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
const params = new URLSearchParams({
|
||||||
type: "web_server",
|
type: "web_server",
|
||||||
|
|
|
@ -105,14 +105,9 @@ export default class BasecampCalendarService implements Calendar {
|
||||||
minute: "numeric",
|
minute: "numeric",
|
||||||
});
|
});
|
||||||
const baseString = `<div>Event title: ${event.title}<br/>Date and time: ${date}, ${startTime} - ${endTime} ${timeZone}<br/>View on Cal.com: <a target="_blank" rel="noreferrer" class="autolinked" data-behavior="truncate" href="https://app.cal.com/booking/${event.uid}">https://app.cal.com/booking/${event.uid}</a> `;
|
const baseString = `<div>Event title: ${event.title}<br/>Date and time: ${date}, ${startTime} - ${endTime} ${timeZone}<br/>View on Cal.com: <a target="_blank" rel="noreferrer" class="autolinked" data-behavior="truncate" href="https://app.cal.com/booking/${event.uid}">https://app.cal.com/booking/${event.uid}</a> `;
|
||||||
const guestString =
|
const guestString = `<br/>Guests: ${event.attendees.reduce((acc, attendee) => {
|
||||||
"<br/>Guests: " +
|
return `${acc}<br/><a target=\"_blank\" rel=\"noreferrer\" class=\"autolinked\" data-behavior=\"truncate\" href=\"mailto:${attendee.email}\">${attendee.email}</a>`;
|
||||||
event.attendees.reduce((acc, attendee) => {
|
}, "")}`;
|
||||||
return (
|
|
||||||
acc +
|
|
||||||
`<br/><a target=\"_blank\" rel=\"noreferrer\" class=\"autolinked\" data-behavior=\"truncate\" href=\"mailto:${attendee.email}\">${attendee.email}</a>`
|
|
||||||
);
|
|
||||||
}, "");
|
|
||||||
|
|
||||||
const videoString = event.videoCallData
|
const videoString = event.videoCallData
|
||||||
? `<br/>Join on video: ${event.videoCallData.url}</div>`
|
? `<br/>Join on video: ${event.videoCallData.url}</div>`
|
||||||
|
|
|
@ -19,4 +19,3 @@ Every CalDav Provider is different and needs minor adjustments.
|
||||||
Follow our CalDav Roadmap here: <a class="text-blue-500" target="_blank" href="https://github.com/calcom/cal.com/issues/3457">https://github.com/calcom/cal.com/issues/3457</a>.
|
Follow our CalDav Roadmap here: <a class="text-blue-500" target="_blank" href="https://github.com/calcom/cal.com/issues/3457">https://github.com/calcom/cal.com/issues/3457</a>.
|
||||||
|
|
||||||
If your CalDav connection is not working, please try another Calendar provider (Google Mail or another CalDav).
|
If your CalDav connection is not working, please try another Calendar provider (Google Mail or another CalDav).
|
||||||
|
|
||||||
|
|
|
@ -49,12 +49,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
let message = e.message;
|
let message = e.message;
|
||||||
if (e.message.indexOf("Invalid credentials") > -1 && url.indexOf("dav.php") > -1) {
|
if (e.message.indexOf("Invalid credentials") > -1 && url.indexOf("dav.php") > -1) {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
const adminUrl =
|
const adminUrl = `${parsedUrl.protocol}//${parsedUrl.hostname}${
|
||||||
parsedUrl.protocol +
|
parsedUrl.port ? `:${parsedUrl.port}` : ""
|
||||||
"//" +
|
}/admin/?/settings/standard/`;
|
||||||
parsedUrl.hostname +
|
|
||||||
(parsedUrl.port ? ":" + parsedUrl.port : "") +
|
|
||||||
"/admin/?/settings/standard/";
|
|
||||||
message = `Couldn\'t connect to caldav account, please verify WebDAV authentication type is set to "Basic"`;
|
message = `Couldn\'t connect to caldav account, please verify WebDAV authentication type is set to "Basic"`;
|
||||||
return res.status(500).json({ message, actionUrl: adminUrl });
|
return res.status(500).json({ message, actionUrl: adminUrl });
|
||||||
}
|
}
|
||||||
|
|
|
@ -71,7 +71,7 @@ export const fetcher = async (endpoint: string, init?: RequestInit | undefined)
|
||||||
return fetch(`https://api.daily.co/v1${endpoint}`, {
|
return fetch(`https://api.daily.co/v1${endpoint}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + api_key,
|
Authorization: `Bearer ${api_key}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...init?.headers,
|
...init?.headers,
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
if (typeof appKeys.client_secret === "string") client_secret = appKeys.client_secret;
|
||||||
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||||
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||||
const redirect_uri = WEBAPP_URL_FOR_OAUTH + "/api/integrations/googlecalendar/callback";
|
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
|
||||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||||
|
|
||||||
const authUrl = oAuth2Client.generateAuthUrl({
|
const authUrl = oAuth2Client.generateAuthUrl({
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
if (!client_id) return res.status(400).json({ message: "Google client_id missing." });
|
||||||
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
if (!client_secret) return res.status(400).json({ message: "Google client_secret missing." });
|
||||||
|
|
||||||
const redirect_uri = WEBAPP_URL_FOR_OAUTH + "/api/integrations/googlecalendar/callback";
|
const redirect_uri = `${WEBAPP_URL_FOR_OAUTH}/api/integrations/googlecalendar/callback`;
|
||||||
|
|
||||||
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
const oAuth2Client = new google.auth.OAuth2(client_id, client_secret, redirect_uri);
|
||||||
|
|
||||||
|
@ -69,7 +69,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
});
|
});
|
||||||
|
|
||||||
res.redirect(
|
res.redirect(
|
||||||
getSafeRedirectUrl(CAL_URL + "/apps/installed/conferencing?hl=google-meet") ??
|
getSafeRedirectUrl(`${CAL_URL}/apps/installed/conferencing?hl=google-meet`) ??
|
||||||
getInstalledAppPath({ variant: "conferencing", slug: "google-meet" })
|
getInstalledAppPath({ variant: "conferencing", slug: "google-meet" })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
if (typeof appKeys.client_id === "string") client_id = appKeys.client_id;
|
||||||
if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." });
|
if (!client_id) return res.status(400).json({ message: "HubSpot client id missing." });
|
||||||
|
|
||||||
const redirectUri = WEBAPP_URL + "/api/integrations/hubspot/callback";
|
const redirectUri = `${WEBAPP_URL}/api/integrations/hubspot/callback`;
|
||||||
const url = hubspotClient.oauth.getAuthorizationUrl(
|
const url = hubspotClient.oauth.getAuthorizationUrl(
|
||||||
client_id,
|
client_id,
|
||||||
redirectUri,
|
redirectUri,
|
||||||
|
|
|
@ -39,7 +39,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
|
const hubspotToken: HubspotToken = await hubspotClient.oauth.tokensApi.createToken(
|
||||||
"authorization_code",
|
"authorization_code",
|
||||||
code,
|
code,
|
||||||
WEBAPP_URL + "/api/integrations/hubspot/callback",
|
`${WEBAPP_URL}/api/integrations/hubspot/callback`,
|
||||||
client_id,
|
client_id,
|
||||||
client_secret
|
client_secret
|
||||||
);
|
);
|
||||||
|
|
|
@ -179,7 +179,7 @@ export default class HubspotCalendarService implements Calendar {
|
||||||
await hubspotClient.oauth.tokensApi.createToken(
|
await hubspotClient.oauth.tokensApi.createToken(
|
||||||
"refresh_token",
|
"refresh_token",
|
||||||
undefined,
|
undefined,
|
||||||
WEBAPP_URL + "/api/integrations/hubspot/callback",
|
`${WEBAPP_URL}/api/integrations/hubspot/callback`,
|
||||||
this.client_id,
|
this.client_id,
|
||||||
this.client_secret,
|
this.client_secret,
|
||||||
refreshToken
|
refreshToken
|
||||||
|
|
|
@ -18,7 +18,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
client_id,
|
client_id,
|
||||||
redirect_uri: WEBAPP_URL_FOR_OAUTH + "/api/integrations/intercom/callback",
|
redirect_uri: `${WEBAPP_URL_FOR_OAUTH}/api/integrations/intercom/callback`,
|
||||||
state,
|
state,
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
};
|
};
|
||||||
|
|
|
@ -48,7 +48,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
log.error("get user_access_token failed", responseBody);
|
log.error("get user_access_token failed", responseBody);
|
||||||
return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody));
|
return res.redirect(`/apps/installed?error=${JSON.stringify(responseBody)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find the admin id from the accompte thanks to access_token and store it
|
// Find the admin id from the accompte thanks to access_token and store it
|
||||||
|
@ -64,7 +64,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
if (admin.status !== 200) {
|
if (admin.status !== 200) {
|
||||||
log.error("get admin_id failed", adminBody);
|
log.error("get admin_id failed", adminBody);
|
||||||
return res.redirect("/apps/installed?error=" + JSON.stringify(adminBody));
|
return res.redirect(`/apps/installed?error=${JSON.stringify(adminBody)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const adminId = adminBody.id;
|
const adminId = adminBody.id;
|
||||||
|
@ -86,7 +86,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
);
|
);
|
||||||
|
|
||||||
res.redirect(
|
res.redirect(
|
||||||
getSafeRedirectUrl(CAL_URL + "/apps/installed/automation?hl=intercom") ??
|
getSafeRedirectUrl(`${CAL_URL}/apps/installed/automation?hl=intercom`) ??
|
||||||
getInstalledAppPath({ variant: "automation", slug: "intercom" })
|
getInstalledAppPath({ variant: "automation", slug: "intercom" })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,9 +6,7 @@
|
||||||
"logo": "icon.svg",
|
"logo": "icon.svg",
|
||||||
"url": "https://github.com/vachmara",
|
"url": "https://github.com/vachmara",
|
||||||
"variant": "automation",
|
"variant": "automation",
|
||||||
"categories": [
|
"categories": ["automation"],
|
||||||
"automation"
|
|
||||||
],
|
|
||||||
"publisher": "Valentin Chmara",
|
"publisher": "Valentin Chmara",
|
||||||
"email": "valentinchmara@gmail.com",
|
"email": "valentinchmara@gmail.com",
|
||||||
"description": "Enhance your scheduling and appointment management experience with the Intercom Integration for Cal.com.",
|
"description": "Enhance your scheduling and appointment management experience with the Intercom Integration for Cal.com.",
|
||||||
|
|
|
@ -33,7 +33,7 @@ const JitsiVideoApiAdapter = (): VideoApiAdapter => {
|
||||||
type: metadata.type,
|
type: metadata.type,
|
||||||
id: meetingID,
|
id: meetingID,
|
||||||
password: "",
|
password: "",
|
||||||
url: hostUrl + "/" + encodeURIComponent(meetingID),
|
url: `${hostUrl}/${encodeURIComponent(meetingID)}`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteMeeting: async (): Promise<void> => {
|
deleteMeeting: async (): Promise<void> => {
|
||||||
|
|
|
@ -22,7 +22,7 @@ async function getHandler(req: NextApiRequest) {
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
app_id,
|
app_id,
|
||||||
redirect_uri: WEBAPP_URL + "/api/integrations/larkcalendar/callback",
|
redirect_uri: `${WEBAPP_URL}/api/integrations/larkcalendar/callback`,
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,7 +28,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const response = await fetch(`https://${LARK_HOST}/open-apis/authen/v1/access_token`, {
|
const response = await fetch(`https://${LARK_HOST}/open-apis/authen/v1/access_token`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + appAccessToken,
|
Authorization: `Bearer ${appAccessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
@ -41,7 +41,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
|
||||||
if (!response.ok || responseBody.code !== 0) {
|
if (!response.ok || responseBody.code !== 0) {
|
||||||
log.error("get user_access_token failed with none 0 code", responseBody);
|
log.error("get user_access_token failed with none 0 code", responseBody);
|
||||||
return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody));
|
return res.redirect(`/apps/installed?error=${JSON.stringify(responseBody)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const key: LarkAuthCredentials = {
|
const key: LarkAuthCredentials = {
|
||||||
|
|
|
@ -114,7 +114,7 @@ export async function sendPostMsg(
|
||||||
const response = await fetch(`https://${LARK_HOST}/open-apis/im/v1/messages?receive_id_type=open_id`, {
|
const response = await fetch(`https://${LARK_HOST}/open-apis/im/v1/messages?receive_id_type=open_id`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + tenantAccessToken,
|
Authorization: `Bearer ${tenantAccessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
@ -122,7 +122,7 @@ export default class LarkCalendarService implements Calendar {
|
||||||
return fetch(`${this.url}${endpoint}`, {
|
return fetch(`${this.url}${endpoint}`, {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...init?.headers,
|
...init?.headers,
|
||||||
},
|
},
|
||||||
|
|
|
@ -402,8 +402,9 @@ export function getSuccessPageLocationMessage(
|
||||||
if (bookingStatus === BookingStatus.CANCELLED || bookingStatus === BookingStatus.REJECTED) {
|
if (bookingStatus === BookingStatus.CANCELLED || bookingStatus === BookingStatus.REJECTED) {
|
||||||
locationToDisplay == t("web_conference");
|
locationToDisplay == t("web_conference");
|
||||||
} else if (isConfirmed) {
|
} else if (isConfirmed) {
|
||||||
locationToDisplay =
|
locationToDisplay = `${getHumanReadableLocationValue(location, t)}: ${t(
|
||||||
getHumanReadableLocationValue(location, t) + ": " + t("meeting_url_in_confirmation_email");
|
"meeting_url_in_confirmation_email"
|
||||||
|
)}`;
|
||||||
} else {
|
} else {
|
||||||
locationToDisplay = t("web_conferencing_details_to_follow");
|
locationToDisplay = t("web_conferencing_details_to_follow");
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
# Setting up Make Integration
|
# Setting up Make Integration
|
||||||
|
|
||||||
|
|
||||||
1. Install the app from the Cal app store and generate an API key. Copy the API key.
|
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.
|
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.
|
3. Create a [Make account](https://www.make.com/en/login), if you don't have one.
|
||||||
|
|
|
@ -22,7 +22,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
return res.status(500).json({ message: "Unable to get bookings." });
|
return res.status(500).json({ message: "Unable to get bookings." });
|
||||||
}
|
}
|
||||||
if (bookings.length === 0) {
|
if (bookings.length === 0) {
|
||||||
const requested = validKey.teamId ? "teamId: " + validKey.teamId : "userId: " + validKey.userId;
|
const requested = validKey.teamId ? `teamId: ${validKey.teamId}` : `userId: ${validKey.userId}`;
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
|
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
|
||||||
});
|
});
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
scope: scopes.join(" "),
|
scope: scopes.join(" "),
|
||||||
client_id,
|
client_id,
|
||||||
redirect_uri: WEBAPP_URL + "/api/integrations/office365calendar/callback",
|
redirect_uri: `${WEBAPP_URL}/api/integrations/office365calendar/callback`,
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
const query = stringify(params);
|
const query = stringify(params);
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
const toUrlEncoded = (payload: Record<string, string>) =>
|
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||||
Object.keys(payload)
|
Object.keys(payload)
|
||||||
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
.map((key) => `${key}=${encodeURIComponent(payload[key])}`)
|
||||||
.join("&");
|
.join("&");
|
||||||
|
|
||||||
const body = toUrlEncoded({
|
const body = toUrlEncoded({
|
||||||
|
@ -37,7 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code,
|
code,
|
||||||
scope: scopes.join(" "),
|
scope: scopes.join(" "),
|
||||||
redirect_uri: WEBAPP_URL + "/api/integrations/office365calendar/callback",
|
redirect_uri: `${WEBAPP_URL}/api/integrations/office365calendar/callback`,
|
||||||
client_secret,
|
client_secret,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -52,11 +52,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const responseBody = await response.json();
|
const responseBody = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody));
|
return res.redirect(`/apps/installed?error=${JSON.stringify(responseBody)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
|
const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
|
||||||
headers: { Authorization: "Bearer " + responseBody.access_token },
|
headers: { Authorization: `Bearer ${responseBody.access_token}` },
|
||||||
});
|
});
|
||||||
const graphUser = await whoami.json();
|
const graphUser = await whoami.json();
|
||||||
|
|
||||||
|
|
|
@ -343,7 +343,7 @@ export default class Office365CalendarService implements Calendar {
|
||||||
return fetch(`${this.apiGraphUrl}${endpoint}`, {
|
return fetch(`${this.apiGraphUrl}${endpoint}`, {
|
||||||
method: "get",
|
method: "get",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + this.accessToken,
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
...init,
|
...init,
|
||||||
|
@ -479,8 +479,8 @@ export default class Office365CalendarService implements Calendar {
|
||||||
subResponse.body.value.reduce((acc: BufferedBusyTime[], evt: BodyValue) => {
|
subResponse.body.value.reduce((acc: BufferedBusyTime[], evt: BodyValue) => {
|
||||||
if (evt.showAs === "free" || evt.showAs === "workingElsewhere") return acc;
|
if (evt.showAs === "free" || evt.showAs === "workingElsewhere") return acc;
|
||||||
return acc.concat({
|
return acc.concat({
|
||||||
start: evt.start.dateTime + "Z",
|
start: `${evt.start.dateTime}Z`,
|
||||||
end: evt.end.dateTime + "Z",
|
end: `${evt.end.dateTime}Z`,
|
||||||
});
|
});
|
||||||
}, [])
|
}, [])
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,7 +20,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
scope: scopes.join(" "),
|
scope: scopes.join(" "),
|
||||||
client_id,
|
client_id,
|
||||||
redirect_uri: WEBAPP_URL + "/api/integrations/office365video/callback",
|
redirect_uri: `${WEBAPP_URL}/api/integrations/office365video/callback`,
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
const query = stringify(params);
|
const query = stringify(params);
|
||||||
|
|
|
@ -30,7 +30,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
const toUrlEncoded = (payload: Record<string, string>) =>
|
const toUrlEncoded = (payload: Record<string, string>) =>
|
||||||
Object.keys(payload)
|
Object.keys(payload)
|
||||||
.map((key) => key + "=" + encodeURIComponent(payload[key]))
|
.map((key) => `${key}=${encodeURIComponent(payload[key])}`)
|
||||||
.join("&");
|
.join("&");
|
||||||
|
|
||||||
const body = toUrlEncoded({
|
const body = toUrlEncoded({
|
||||||
|
@ -38,7 +38,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
grant_type: "authorization_code",
|
grant_type: "authorization_code",
|
||||||
code,
|
code,
|
||||||
scope: scopes.join(" "),
|
scope: scopes.join(" "),
|
||||||
redirect_uri: WEBAPP_URL + "/api/integrations/office365video/callback",
|
redirect_uri: `${WEBAPP_URL}/api/integrations/office365video/callback`,
|
||||||
client_secret,
|
client_secret,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -53,11 +53,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const responseBody = await response.json();
|
const responseBody = await response.json();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
return res.redirect("/apps/installed?error=" + JSON.stringify(responseBody));
|
return res.redirect(`/apps/installed?error=${JSON.stringify(responseBody)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
|
const whoami = await fetch("https://graph.microsoft.com/v1.0/me", {
|
||||||
headers: { Authorization: "Bearer " + responseBody.access_token },
|
headers: { Authorization: `Bearer ${responseBody.access_token}` },
|
||||||
});
|
});
|
||||||
const graphUser = await whoami.json();
|
const graphUser = await whoami.json();
|
||||||
|
|
||||||
|
|
|
@ -128,7 +128,7 @@ const TeamsVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
|
||||||
const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", {
|
const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(translateEvent(event)),
|
body: JSON.stringify(translateEvent(event)),
|
||||||
|
@ -152,7 +152,7 @@ const TeamsVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
|
||||||
const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", {
|
const resultString = await fetch("https://graph.microsoft.com/v1.0/me/onlineMeetings", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify(translateEvent(event)),
|
body: JSON.stringify(translateEvent(event)),
|
||||||
|
|
|
@ -272,7 +272,7 @@ class Paypal {
|
||||||
webhook_id: options.body.webhook_id,
|
webhook_id: options.body.webhook_id,
|
||||||
});
|
});
|
||||||
|
|
||||||
const bodyToString = stringy.slice(0, -1) + `,"webhook_event":${options.body.webhook_event}` + "}";
|
const bodyToString = `${stringy.slice(0, -1)},"webhook_event":${options.body.webhook_event}}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await this.fetcher(`/v1/notifications/verify-webhook-signature`, {
|
const response = await this.fetcher(`/v1/notifications/verify-webhook-signature`, {
|
||||||
|
|
|
@ -19,7 +19,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
const eventTypeURL = eventType.URL + query;
|
const eventTypeURL = eventType.URL + query;
|
||||||
|
|
||||||
function QRCode({ size, data }: { size: number; data: string }) {
|
function QRCode({ size, data }: { size: number; data: string }) {
|
||||||
const QR_URL = "https://api.qrserver.com/v1/create-qr-code/?size=" + size + "&data=" + data;
|
const QR_URL = `https://api.qrserver.com/v1/create-qr-code/?size=${size}&data=${data}`;
|
||||||
return (
|
return (
|
||||||
<Tooltip content={eventTypeURL}>
|
<Tooltip content={eventTypeURL}>
|
||||||
<a download href={QR_URL} target="_blank" rel="noreferrer">
|
<a download href={QR_URL} target="_blank" rel="noreferrer">
|
||||||
|
|
|
@ -108,10 +108,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
// Make Header
|
// Make Header
|
||||||
res.write(
|
res.write(
|
||||||
headerFields
|
`${headerFields
|
||||||
.map((field) => `${field.label}${field.deleted ? "(Deleted)" : ""}`)
|
.map((field) => `${field.label}${field.deleted ? "(Deleted)" : ""}`)
|
||||||
.concat(["Submission Time"])
|
.concat(["Submission Time"])
|
||||||
.join(",") + "\n"
|
.join(",")}\n`
|
||||||
);
|
);
|
||||||
|
|
||||||
for await (const partialCsv of csvIterator) {
|
for await (const partialCsv of csvIterator) {
|
||||||
|
|
|
@ -44,7 +44,7 @@ export function getQueryBuilderConfig(form: RoutingForm, forReporting = false) {
|
||||||
// preferWidgets: field.type === "textarea" ? ["textarea"] : [],
|
// preferWidgets: field.type === "textarea" ? ["textarea"] : [],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unsupported field type:" + field.type);
|
throw new Error(`Unsupported field type:${field.type}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export async function getSerializableForm<TForm extends App_RoutingForms_Form>({
|
||||||
const fieldsParsed = zodFields.safeParse(form.fields);
|
const fieldsParsed = zodFields.safeParse(form.fields);
|
||||||
|
|
||||||
if (!fieldsParsed.success) {
|
if (!fieldsParsed.success) {
|
||||||
throw new Error("Error parsing fields" + fieldsParsed.error);
|
throw new Error(`Error parsing fields: ${fieldsParsed.error}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const settings = RoutingFormSettings.parse(
|
const settings = RoutingFormSettings.parse(
|
||||||
|
@ -91,7 +91,7 @@ export async function getSerializableForm<TForm extends App_RoutingForms_Form>({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (!router) {
|
if (!router) {
|
||||||
throw new Error("Form -" + route.id + ", being used as router, not found");
|
throw new Error(`Form - ${route.id}, being used as router, not found`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsedRouter = await getSerializableForm({ form: router });
|
const parsedRouter = await getSerializableForm({ form: router });
|
||||||
|
|
|
@ -241,7 +241,7 @@ export default function RoutingForms({
|
||||||
<ArrowButton onClick={() => moveRoutingForm(index, 1)} arrowDirection="down" />
|
<ArrowButton onClick={() => moveRoutingForm(index, 1)} arrowDirection="down" />
|
||||||
)}
|
)}
|
||||||
<ListLinkItem
|
<ListLinkItem
|
||||||
href={appUrl + "/form-edit/" + form.id}
|
href={`${appUrl}/form-edit/${form.id}`}
|
||||||
heading={form.name}
|
heading={form.name}
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
subHeading={description}
|
subHeading={description}
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const conn = new jsforce.Connection({
|
const conn = new jsforce.Connection({
|
||||||
clientId: consumer_key,
|
clientId: consumer_key,
|
||||||
clientSecret: consumer_secret,
|
clientSecret: consumer_secret,
|
||||||
redirectUri: WEBAPP_URL + "/api/integrations/salesforce/callback",
|
redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);
|
const salesforceTokenInfo = await conn.oauth2.requestToken(code as string);
|
||||||
|
|
|
@ -110,7 +110,7 @@ export default class SalesforceCalendarService implements Calendar {
|
||||||
return new jsforce.Connection({
|
return new jsforce.Connection({
|
||||||
clientId: consumer_key,
|
clientId: consumer_key,
|
||||||
clientSecret: consumer_secret,
|
clientSecret: consumer_secret,
|
||||||
redirectUri: WEBAPP_URL + "/api/integrations/salesforce/callback",
|
redirectUri: `${WEBAPP_URL}/api/integrations/salesforce/callback`,
|
||||||
instanceUrl: credentialKey.instance_url,
|
instanceUrl: credentialKey.instance_url,
|
||||||
accessToken: credentialKey.access_token,
|
accessToken: credentialKey.access_token,
|
||||||
refreshToken: credentialKey.refresh_token,
|
refreshToken: credentialKey.refresh_token,
|
||||||
|
|
|
@ -23,7 +23,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const redirect_uri = encodeURI(WEBAPP_URL + "/api/integrations/stripepayment/callback");
|
const redirect_uri = encodeURI(`${WEBAPP_URL}/api/integrations/stripepayment/callback`);
|
||||||
const stripeConnectParams: Stripe.OAuthAuthorizeUrlParams = {
|
const stripeConnectParams: Stripe.OAuthAuthorizeUrlParams = {
|
||||||
client_id,
|
client_id,
|
||||||
scope: "read_write",
|
scope: "read_write",
|
||||||
|
|
|
@ -22,7 +22,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
const query = stringify({ error, error_description });
|
const query = stringify({ error, error_description });
|
||||||
res.redirect("/apps/installed?" + query);
|
res.redirect(`/apps/installed?${query}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -60,7 +60,7 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppCard
|
<AppCard
|
||||||
returnTo={WEBAPP_URL + pathname + "?tabName=apps"}
|
returnTo={`${WEBAPP_URL}${pathname}?tabName=apps`}
|
||||||
app={app}
|
app={app}
|
||||||
switchChecked={requirePayment}
|
switchChecked={requirePayment}
|
||||||
switchOnClick={(enabled) => {
|
switchOnClick={(enabled) => {
|
||||||
|
|
|
@ -15,5 +15,5 @@ export function createPaymentLink(opts: {
|
||||||
let link = "";
|
let link = "";
|
||||||
if (absolute) link = WEBSITE_URL;
|
if (absolute) link = WEBSITE_URL;
|
||||||
const query = stringify({ date, name, email });
|
const query = stringify({ date, name, email });
|
||||||
return link + `/payment/${paymentUid}?${query}`;
|
return `${link}/payment/${paymentUid}?${query}`;
|
||||||
}
|
}
|
||||||
|
|
|
@ -73,7 +73,7 @@ export async function deleteStripeCustomer(user: UserType): Promise<string | nul
|
||||||
const customerId = await getStripeCustomerId(user);
|
const customerId = await getStripeCustomerId(user);
|
||||||
|
|
||||||
if (!customerId) {
|
if (!customerId) {
|
||||||
console.warn("No stripe customer found for user:" + user.email);
|
console.warn(`No stripe customer found for user:${user.email}`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ const SylapsApiAdapter = (): VideoApiAdapter => {
|
||||||
type: "sylaps_video",
|
type: "sylaps_video",
|
||||||
id: meetingID,
|
id: meetingID,
|
||||||
password: "",
|
password: "",
|
||||||
url: "https://sylaps.com/r/" + meetingID,
|
url: `https://sylaps.com/r/${meetingID}`,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteMeeting: async (): Promise<void> => {
|
deleteMeeting: async (): Promise<void> => {
|
||||||
|
|
|
@ -27,7 +27,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
if (!client_id) return res.status(400).json({ message: "Tandem client_id missing." });
|
if (!client_id) return res.status(400).json({ message: "Tandem client_id missing." });
|
||||||
if (!base_url) return res.status(400).json({ message: "Tandem base_url missing." });
|
if (!base_url) return res.status(400).json({ message: "Tandem base_url missing." });
|
||||||
|
|
||||||
const redirect_uri = encodeURI(WEBAPP_URL + "/api/integrations/tandemvideo/callback");
|
const redirect_uri = encodeURI(`${WEBAPP_URL}/api/integrations/tandemvideo/callback`);
|
||||||
|
|
||||||
const params = {
|
const params = {
|
||||||
client_id,
|
client_id,
|
||||||
|
|
|
@ -125,7 +125,7 @@ const TandemVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =
|
||||||
const result = await fetch(`${base_url}/api/v1/meetings`, {
|
const result = await fetch(`${base_url}/api/v1/meetings`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: _translateEvent(event, "meeting"),
|
body: _translateEvent(event, "meeting"),
|
||||||
|
@ -141,7 +141,7 @@ const TandemVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =
|
||||||
await fetch(`${base_url}/api/v1/meetings/${uid}`, {
|
await fetch(`${base_url}/api/v1/meetings/${uid}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
},
|
},
|
||||||
}).then(handleErrorsRaw);
|
}).then(handleErrorsRaw);
|
||||||
|
|
||||||
|
@ -154,7 +154,7 @@ const TandemVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =
|
||||||
const result = await fetch(`${base_url}/api/v1/meetings/${bookingRef.meetingId}`, {
|
const result = await fetch(`${base_url}/api/v1/meetings/${bookingRef.meetingId}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: _translateEvent(event, "updates"),
|
body: _translateEvent(event, "updates"),
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
items:
|
items:
|
||||||
- 1.jpg
|
- 1.jpg
|
||||||
---
|
---
|
||||||
|
|
||||||
Vital App is an app that can can help you combine your health peripherals with your calendar.
|
Vital App is an app that can can help you combine your health peripherals with your calendar.
|
||||||
|
|
||||||
#### Supported Actions:
|
#### Supported Actions:
|
||||||
|
|
|
@ -45,7 +45,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const token = await vitalClient.Link.create(
|
const token = await vitalClient.Link.create(
|
||||||
userVital?.user_id,
|
userVital?.user_id,
|
||||||
undefined,
|
undefined,
|
||||||
WEBAPP_URL + "/api/integrations/vital/callback"
|
`${WEBAPP_URL}/api/integrations/vital/callback`
|
||||||
);
|
);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
token: token.link_token,
|
token: token.link_token,
|
||||||
|
|
|
@ -15,7 +15,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
/** @link https://developer.webex.com/docs/integrations#getting-an-access-token **/
|
/** @link https://developer.webex.com/docs/integrations#getting-an-access-token **/
|
||||||
|
|
||||||
const redirectUri = encodeURI(`${WEBAPP_URL}/api/integrations/${config.slug}/callback`);
|
const redirectUri = encodeURI(`${WEBAPP_URL}/api/integrations/${config.slug}/callback`);
|
||||||
const authHeader = "Basic " + Buffer.from(client_id + ":" + client_secret).toString("base64");
|
const authHeader = `Basic ${Buffer.from(`${client_id}:${client_secret}`).toString("base64")}`;
|
||||||
const result = await fetch(
|
const result = await fetch(
|
||||||
`https://webexapis.com/v1/access_token?grant_type=authorization_code&client_id=${client_id}&client_secret=${client_secret}&code=${code}&redirect_uri=${redirectUri}`,
|
`https://webexapis.com/v1/access_token?grant_type=authorization_code&client_id=${client_id}&client_secret=${client_secret}&code=${code}&redirect_uri=${redirectUri}`,
|
||||||
{
|
{
|
||||||
|
|
|
@ -154,7 +154,7 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
|
||||||
method: "GET",
|
method: "GET",
|
||||||
...options,
|
...options,
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: "Bearer " + accessToken,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
...options?.headers,
|
...options?.headers,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -208,7 +208,7 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
|
||||||
url: result.webLink,
|
url: result.webLink,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error("Failed to create meeting. Response is " + JSON.stringify(result));
|
throw new Error(`Failed to create meeting. Response is ${JSON.stringify(result)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw new Error("Unexpected error");
|
throw new Error("Unexpected error");
|
||||||
|
@ -258,7 +258,7 @@ const WebexVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter =>
|
||||||
url: result.webLink,
|
url: result.webLink,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw new Error("Failed to create meeting. Response is " + JSON.stringify(result));
|
throw new Error(`Failed to create meeting. Response is ${JSON.stringify(result)}`);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
throw new Error("Unexpected error");
|
throw new Error("Unexpected error");
|
||||||
|
|
|
@ -27,7 +27,7 @@ const wipeMyCalAction = async (props: IWipeMyCalAction) => {
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const endpoint = "/api/integrations/wipemycalother/wipe";
|
const endpoint = "/api/integrations/wipemycalother/wipe";
|
||||||
return fetch(`${process.env.NEXT_PUBLIC_WEBAPP_URL}` + endpoint, {
|
return fetch(`${process.env.NEXT_PUBLIC_WEBAPP_URL}${endpoint}`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
|
|
|
@ -26,7 +26,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
? authorizedAccount.name
|
? authorizedAccount.name
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const requested = teamInfo ? "team: " + teamInfo : "user: " + userInfo;
|
const requested = teamInfo ? `team: ${teamInfo}` : `user: ${userInfo}`;
|
||||||
return res.status(404).json({
|
return res.status(404).json({
|
||||||
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
|
message: `There are no bookings to retrieve, please create a booking first. Requested: \`${requested}\``,
|
||||||
});
|
});
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
const clientId = typeof appKeys.client_id === "string" ? appKeys.client_id : "";
|
const clientId = typeof appKeys.client_id === "string" ? appKeys.client_id : "";
|
||||||
if (!clientId) return res.status(400).json({ message: "Zoho Bigin client_id missing." });
|
if (!clientId) return res.status(400).json({ message: "Zoho Bigin client_id missing." });
|
||||||
|
|
||||||
const redirectUri = WEBAPP_URL + `/api/integrations/zoho-bigin/callback`;
|
const redirectUri = `${WEBAPP_URL}/api/integrations/zoho-bigin/callback`;
|
||||||
|
|
||||||
const authUrl = axios.getUri({
|
const authUrl = axios.getUri({
|
||||||
url: "https://accounts.zoho.com/oauth/v2/auth",
|
url: "https://accounts.zoho.com/oauth/v2/auth",
|
||||||
|
|
|
@ -33,7 +33,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||||
if (!clientSecret) return res.status(400).json({ message: "Zoho Bigin client_secret missing." });
|
if (!clientSecret) return res.status(400).json({ message: "Zoho Bigin client_secret missing." });
|
||||||
|
|
||||||
const accountsUrl = `${accountsServer}/oauth/v2/token`;
|
const accountsUrl = `${accountsServer}/oauth/v2/token`;
|
||||||
const redirectUri = WEBAPP_URL + `/api/integrations/${appConfig.slug}/callback`;
|
const redirectUri = `${WEBAPP_URL}/api/integrations/${appConfig.slug}/callback`;
|
||||||
|
|
||||||
const formData = {
|
const formData = {
|
||||||
client_id: clientId,
|
client_id: clientId,
|
||||||
|
|
|
@ -148,8 +148,9 @@ export default class BiginCalendarService implements Calendar {
|
||||||
*/
|
*/
|
||||||
private async contactSearch(event: CalendarEvent) {
|
private async contactSearch(event: CalendarEvent) {
|
||||||
const token = await this.auth.getToken();
|
const token = await this.auth.getToken();
|
||||||
const searchCriteria =
|
const searchCriteria = `(${event.attendees
|
||||||
"(" + event.attendees.map((attendee) => `(Email:equals:${encodeURI(attendee.email)})`).join("or") + ")";
|
.map((attendee) => `(Email:equals:${encodeURI(attendee.email)})`)
|
||||||
|
.join("or")})`;
|
||||||
|
|
||||||
return await axios({
|
return await axios({
|
||||||
method: "get",
|
method: "get",
|
||||||
|
@ -298,21 +299,9 @@ const toISO8601String = (date: Date) => {
|
||||||
return (num < 10 ? "0" : "") + num;
|
return (num < 10 ? "0" : "") + num;
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}T${pad(
|
||||||
date.getFullYear() +
|
date.getHours()
|
||||||
"-" +
|
)}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${dif}${pad(Math.floor(Math.abs(tzo) / 60))}:${pad(
|
||||||
pad(date.getMonth() + 1) +
|
Math.abs(tzo) % 60
|
||||||
"-" +
|
)}`;
|
||||||
pad(date.getDate()) +
|
|
||||||
"T" +
|
|
||||||
pad(date.getHours()) +
|
|
||||||
":" +
|
|
||||||
pad(date.getMinutes()) +
|
|
||||||
":" +
|
|
||||||
pad(date.getSeconds()) +
|
|
||||||
dif +
|
|
||||||
pad(Math.floor(Math.abs(tzo) / 60)) +
|
|
||||||
":" +
|
|
||||||
pad(Math.abs(tzo) % 60)
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,13 +4,13 @@
|
||||||
|
|
||||||
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
||||||
2. Create a "Server-based Applications", set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocalendar/callback` replacing Cal.com URL with the URI at which your application runs.
|
2. Create a "Server-based Applications", set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocalendar/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||||
4. Fill in any information you want in the "Client Details" tab
|
3. Fill in any information you want in the "Client Details" tab
|
||||||
5. Go to tab "Client Secret" tab.
|
4. Go to tab "Client Secret" tab.
|
||||||
6. Now copy the Client ID and Client Secret into your app keys in the Cal.com admin panel (`<Cal.com>/settings/admin/apps`).
|
5. Now copy the Client ID and Client Secret into your app keys in the Cal.com admin panel (`<Cal.com>/settings/admin/apps`).
|
||||||
7. Back in Zoho API Console,
|
6. Back in Zoho API Console,
|
||||||
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
7. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
||||||
9. Click the "Save"/ "UPDATE" button at the bottom footer.
|
8. Click the "Save"/ "UPDATE" button at the bottom footer.
|
||||||
10. You're good to go. Now you can easily add your Zoho Calendar integration in the Cal.com settings at `/settings/my-account/calendars`.
|
9. You're good to go. Now you can easily add your Zoho Calendar integration in the Cal.com settings at `/settings/my-account/calendars`.
|
||||||
11. You can access your Zoho calendar at [https://calendar.zoho.com/](https://calendar.zoho.com/)
|
10. You can access your Zoho calendar at [https://calendar.zoho.com/](https://calendar.zoho.com/)
|
||||||
|
|
||||||
NOTE: If you use multiple calendars with Cal, make sure you enable the toggle to prevent double-bookings across calendar. This is in `/settings/my-account/calendars`.
|
NOTE: If you use multiple calendars with Cal, make sure you enable the toggle to prevent double-bookings across calendar. This is in `/settings/my-account/calendars`.
|
|
@ -20,7 +20,7 @@ async function getHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const params = {
|
const params = {
|
||||||
client_id,
|
client_id,
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
redirect_uri: WEBAPP_URL + "/api/integrations/zohocalendar/callback",
|
redirect_uri: `${WEBAPP_URL}/api/integrations/zohocalendar/callback`,
|
||||||
scope: [
|
scope: [
|
||||||
"ZohoCalendar.calendar.ALL",
|
"ZohoCalendar.calendar.ALL",
|
||||||
"ZohoCalendar.event.ALL",
|
"ZohoCalendar.event.ALL",
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue