Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7aa3c7163 | ||
|
|
e899a92071 | ||
|
|
ebdbe407cd | ||
|
|
e7f917bab9 | ||
|
|
ba9688a04a | ||
|
|
f0e15d77dc | ||
|
|
30b72b687e | ||
|
|
bbf50dbc53 | ||
|
|
bb547a0111 | ||
|
|
1758288948 | ||
|
|
11789c725a | ||
|
|
73bfe0aebe | ||
|
|
24140f0f29 | ||
|
|
e504f5cd9e | ||
|
|
72629ffee0 | ||
|
|
5b6f007d8f | ||
|
|
2e0951d4dc | ||
|
|
fd84b5754d | ||
|
|
73afb03308 | ||
|
|
99dba73d0b | ||
|
|
db4449d849 | ||
|
|
53e52a0510 | ||
|
|
cf4bd8a77d | ||
|
|
9d61dc2f7c | ||
|
|
caf5918b72 | ||
|
|
84f64f5d06 | ||
|
|
13a53c020b | ||
|
|
a72f90fe02 | ||
|
|
283d5b4a65 | ||
|
|
81b2e37efd | ||
|
|
469967b320 | ||
|
|
d708625da0 | ||
|
|
acf2487eaa | ||
|
|
b81ca0e2ab | ||
|
|
2357f76cc8 | ||
|
|
304e8aaa68 | ||
|
|
9fe912c392 | ||
|
|
ab7ecff580 | ||
|
|
19e830acad | ||
|
|
458300bc83 | ||
|
|
352a467104 | ||
|
|
c052c66abc | ||
|
|
8f8ed4d80d | ||
|
|
04fb7e07b4 | ||
|
|
20d2129f81 | ||
|
|
ae5425f908 | ||
|
|
762810684e | ||
|
|
4720dc410f | ||
|
|
715aea5733 | ||
|
|
6cbb9bf967 | ||
|
|
d8176b2eb4 | ||
|
|
6c51e2a970 | ||
|
|
3a08eb3dca | ||
|
|
cee905bc5a | ||
|
|
72fd6334c6 | ||
|
|
63f51abd84 | ||
|
|
6e856b3bd1 | ||
|
|
5fceee27fe | ||
|
|
4f590ebbf7 | ||
|
|
736cd63509 | ||
|
|
3aaa1cde0d | ||
|
|
02d32321aa | ||
|
|
697f6db2c4 | ||
|
|
83364a9e97 | ||
|
|
1620ae660f | ||
|
|
605f9c61f3 | ||
|
|
564862369f | ||
|
|
be6cf4705c | ||
|
|
a2457ab619 | ||
|
|
867966dd75 | ||
|
|
08ad4c07ea | ||
|
|
78da57055e | ||
|
|
d5c3d9d967 | ||
|
|
aa0e9e6a02 | ||
|
|
cad97f1779 | ||
|
|
31ee689672 | ||
|
|
3edb6f5ffd | ||
|
|
a8825badec | ||
|
|
04c634ec4b | ||
|
|
8dd9b8d74b | ||
|
|
0365157638 | ||
|
|
a571d40958 | ||
|
|
69491d5700 | ||
|
|
20022e2cdd | ||
|
|
492858b45b | ||
|
|
b49ad8294b | ||
|
|
aa30ff7d1a | ||
|
|
cfa720a523 | ||
|
|
9ab196c30e | ||
|
|
47e13cd2fd | ||
|
|
74ab6d209e | ||
|
|
46f0b55a45 | ||
|
|
e4893c2218 | ||
|
|
fa17139e8f | ||
|
|
1cdc97dd37 | ||
|
|
f630e8af40 | ||
|
|
fe0745121b | ||
|
|
f6d7568d0a | ||
|
|
520e7fe036 | ||
|
|
1bf50e09cd | ||
|
|
d1ca7b4bdd | ||
|
|
4e2473531f |
@@ -12,13 +12,13 @@ Fixes # (issue)
|
|||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
<!-- Please delete options that are not relevant. -->
|
<!-- Please delete bullets that are not relevant. -->
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
- Chore (refactoring code, technical debt, workflow improvements)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
- [ ] This change requires a documentation update
|
- This change requires a documentation update
|
||||||
|
|
||||||
## How should this be tested?
|
## How should this be tested?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "tsc-absolute",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "(?:^|\\s)([^\\s].*)[\\(:](\\d+)[,:](\\d+)(?:\\):\\s+|\\s+-\\s+)(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"code": 5,
|
||||||
|
"message": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
name: "Apply issue labels to PR"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
label_on_pr:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: 16
|
||||||
|
|
||||||
|
- name: Apply labels from linked issue to PR
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
PR_DATA: ${{ toJson(github.event.pull_request) }}
|
||||||
|
run: node ./.github/workflows/scripts/apply-issue-labels-to-pr.ts
|
||||||
@@ -10,8 +10,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
|
|
||||||
- name: Show info
|
- name: Show info
|
||||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||||
|
- name: Configure TSC problem matcher
|
||||||
- run: yarn type-check
|
run: |
|
||||||
|
echo "::remove-matcher owner=tsc::"
|
||||||
|
echo "::add-matcher::.github/matchers/tsc-absolute.json"
|
||||||
|
- run: yarn type-check:ci
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
name: Cron - downgradeUsers
|
name: Cron - downgradeUsers
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||||
schedule:
|
schedule:
|
||||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
# Runs “Every month at 1st (see https://crontab.guru)
|
||||||
- cron: "0,15,30,45 * * * *"
|
- cron: "0 0 1 * *"
|
||||||
jobs:
|
jobs:
|
||||||
cron-downgradeUsers:
|
cron-downgradeUsers:
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -26,6 +26,6 @@ jobs:
|
|||||||
localization_branch_name: main
|
localization_branch_name: main
|
||||||
create_pull_request: false
|
create_pull_request: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||||
|
|||||||
@@ -0,0 +1,149 @@
|
|||||||
|
const https = require("https");
|
||||||
|
|
||||||
|
async function applyLabelFromLinkedIssueToPR(pr, token) {
|
||||||
|
|
||||||
|
// Get the labels from issues linked to the PR
|
||||||
|
const query = `
|
||||||
|
query GetLinkedIssues($owner: String!, $repo: String!, $prNumber: Int!) {
|
||||||
|
repository(owner: $owner, name: $repo) {
|
||||||
|
pullRequest(number: $prNumber) {
|
||||||
|
closingIssuesReferences(first: 10) {
|
||||||
|
nodes {
|
||||||
|
number
|
||||||
|
labels(first: 10) {
|
||||||
|
nodes {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const graphqlData = JSON.stringify({
|
||||||
|
query,
|
||||||
|
variables: {
|
||||||
|
owner: pr.base.repo.owner.login,
|
||||||
|
repo: pr.base.repo.name,
|
||||||
|
prNumber: pr.number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
hostname: "api.github.com",
|
||||||
|
path: "/graphql",
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": graphqlData.length,
|
||||||
|
"Authorization": "Bearer " + token,
|
||||||
|
"User-Agent": "Node.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use the native Node.js request library to make the request
|
||||||
|
let linkedIssues;
|
||||||
|
linkedIssues = await new Promise((resolve, reject) => {
|
||||||
|
const request = https.request(requestOptions, (response) => {
|
||||||
|
let responseBody = "";
|
||||||
|
response.on("data", (chunk) => {
|
||||||
|
responseBody += chunk;
|
||||||
|
});
|
||||||
|
response.on("end", () => {
|
||||||
|
resolve(
|
||||||
|
JSON.parse(responseBody)
|
||||||
|
?.data
|
||||||
|
?.repository
|
||||||
|
?.pullRequest
|
||||||
|
?.closingIssuesReferences
|
||||||
|
?.nodes
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.write(graphqlData);
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!linkedIssues || linkedIssues.length === 0) {
|
||||||
|
console.log("No issue linked.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Iterate over linked issues and apply labels to PR
|
||||||
|
for (const issue of linkedIssues) {
|
||||||
|
const labels = issue?.labels?.nodes?.map((label) => label.name);
|
||||||
|
|
||||||
|
if (!labels || labels.length === 0) {
|
||||||
|
console.log(`No labels found on the linked issue #${issue.number}.`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelsData = JSON.stringify({
|
||||||
|
labels: labels,
|
||||||
|
});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
hostname: "api.github.com",
|
||||||
|
path: `/repos/${pr.base.repo.owner.login}/${pr.base.repo.name}/issues/${pr.number}/labels`,
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Content-Length": labelsData.length,
|
||||||
|
"Authorization": "Bearer " + token,
|
||||||
|
"User-Agent": "Node.js",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let labelResult;
|
||||||
|
labelResult = await new Promise((resolve, reject) => {
|
||||||
|
const request = https.request(requestOptions, (response) => {
|
||||||
|
let responseBody = "";
|
||||||
|
response.on("data", (chunk) => {
|
||||||
|
responseBody += chunk;
|
||||||
|
});
|
||||||
|
response.on("data", () => {});
|
||||||
|
response.on("end", () => {
|
||||||
|
resolve(JSON.parse(responseBody));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
request.on("error", (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
request.write(labelsData);
|
||||||
|
request.end();
|
||||||
|
});
|
||||||
|
|
||||||
|
if (labelResult.message) {
|
||||||
|
console.log(`Error labelling PR: ${labelResult.message}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`Applied labels: ${labels.join(", ")} to PR #${
|
||||||
|
pr.number
|
||||||
|
} from linked issue #${issue.number}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pass the PR data and GitHub token to the script
|
||||||
|
(async () => {
|
||||||
|
if (!process.env.PR_DATA) {
|
||||||
|
console.log("No PR data found.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const prData = JSON.parse(process.env.PR_DATA);
|
||||||
|
const token = process.env.GITHUB_TOKEN;
|
||||||
|
|
||||||
|
await applyLabelFromLinkedIssueToPR(prData, token);
|
||||||
|
})();
|
||||||
@@ -350,8 +350,7 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
|
|||||||
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
|
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
|
||||||
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTRIBUTORS -->
|
<!-- CONTRIBUTORS -->
|
||||||
|
|
||||||
@@ -456,12 +455,12 @@ following
|
|||||||
|
|
||||||
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. From within the API console page, go to "Applications".
|
2. From within the API console page, go to "Applications".
|
||||||
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
||||||
4. Fill in any information you want in the "Client Details" tab
|
4. Fill in any information you want in the "Client Details" tab
|
||||||
5. Go to tab "Client Secret" tab.
|
5. Go to tab "Client Secret" tab.
|
||||||
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
|
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
|
||||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
|
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||||
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
8. 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.
|
9. Click the "Save"/ "UPDATE" button at the bottom footer.
|
||||||
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
||||||
"start": "PORT=4000 next start",
|
"start": "PORT=4000 next start",
|
||||||
"type-check": "tsc --pretty --noEmit"
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"author": "Cal.com, Inc.",
|
"author": "Cal.com, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
import type { OptionProps } from "react-select";
|
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import type { App } from "@calcom/types/App";
|
|
||||||
import { Button } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
|
|
||||||
interface AdditionalCalendarSelectorProps {
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
|
|
||||||
const { data } = optionProps;
|
|
||||||
return (
|
|
||||||
<InstallAppButton
|
|
||||||
type={data.type}
|
|
||||||
render={(installProps) => {
|
|
||||||
return (
|
|
||||||
<Button {...installProps} className="w-full" color="minimal">
|
|
||||||
{/* eslint-disable @next/next/no-img-element */}
|
|
||||||
{data.image && (
|
|
||||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
|
||||||
)}
|
|
||||||
<p>{data.label}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryCell
|
|
||||||
query={query}
|
|
||||||
success={({ data }) => {
|
|
||||||
const options = data.items.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
slug: item.slug,
|
|
||||||
image: item.logo,
|
|
||||||
type: item.type,
|
|
||||||
}));
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
name="additionalCalendar"
|
|
||||||
placeholder={t("connect_additional_calendar")}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
color: "#3E3E3E",
|
|
||||||
marginLeft: "3px",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "2px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
maxWidth: "320px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
|
|
||||||
isLoading={isLoading}
|
|
||||||
components={{ Option: ImageOption }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdditionalCalendarSelector;
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { DestinationCalendar } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onChange: (value: { externalId: string; integration: string }) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
hidePlaceholder?: boolean;
|
|
||||||
/** The external Id of the connected calendar */
|
|
||||||
value: string | undefined;
|
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DestinationCalendarSelector = ({
|
|
||||||
onChange,
|
|
||||||
isLoading,
|
|
||||||
value,
|
|
||||||
hidePlaceholder,
|
|
||||||
destinationCalendar,
|
|
||||||
}: Props): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
|
||||||
|
|
||||||
// Extra styles to show prefixed text in react-select
|
|
||||||
const content = (hidePlaceholder = false) => {
|
|
||||||
if (!hidePlaceholder) {
|
|
||||||
return {
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
":before": {
|
|
||||||
content: `'${t("select_destination_calendar")}:'`,
|
|
||||||
display: "block",
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selected = query.data?.connectedCalendars
|
|
||||||
.map((connected) => connected.calendars ?? [])
|
|
||||||
.flat()
|
|
||||||
.find((cal) => cal.externalId === value);
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
setSelectedOption({
|
|
||||||
value: `${selected.integration}:${selected.externalId}`,
|
|
||||||
label: selected.name || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [query.data?.connectedCalendars, value]);
|
|
||||||
|
|
||||||
if (!query.data?.connectedCalendars.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const options =
|
|
||||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
|
||||||
key: selectedCalendar.credentialId,
|
|
||||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
|
||||||
options: (selectedCalendar.calendars ?? [])
|
|
||||||
.filter((cal) => cal.readOnly === false)
|
|
||||||
.map((cal) => ({
|
|
||||||
label: cal.name || "",
|
|
||||||
value: `${cal.integration}:${cal.externalId}`,
|
|
||||||
})),
|
|
||||||
})) ?? [];
|
|
||||||
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
|
||||||
? `(${
|
|
||||||
destinationCalendar.externalId.length > 15
|
|
||||||
? destinationCalendar.externalId.substring(0, 15) + "..."
|
|
||||||
: destinationCalendar.externalId
|
|
||||||
})`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
|
||||||
<Select
|
|
||||||
name="primarySelectedCalendar"
|
|
||||||
placeholder={
|
|
||||||
!hidePlaceholder ? (
|
|
||||||
`${t("select_destination_calendar")}`
|
|
||||||
) : (
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "6px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className={classNames(
|
|
||||||
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
|
||||||
!hidePlaceholder && "font-medium"
|
|
||||||
)}
|
|
||||||
onChange={(option) => {
|
|
||||||
setSelectedOption(option);
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
|
||||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
integration,
|
|
||||||
externalId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
value={selectedOption}
|
|
||||||
components={{
|
|
||||||
IndicatorSeparator: () => null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DestinationCalendarSelector;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { NextRouter } from "next/router";
|
import type { NextRouter } from "next/router";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
import { FiCode, FiTrello, FiSun, FiArrowLeft } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
|
|||||||
const height = getDimension(previewState.inline.height);
|
const height = getDimension(previewState.inline.height);
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
@@ -187,12 +187,12 @@ function MyComponent() {
|
|||||||
}) => {
|
}) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
const cal = await getCalApi();
|
const cal = await getCalApi();
|
||||||
Cal("floatingButton", ${floatingButtonArg});
|
cal("floatingButton", ${floatingButtonArg});
|
||||||
${uiInstructionCode}
|
${uiInstructionCode}
|
||||||
})();
|
})();
|
||||||
}, [])
|
}, [])
|
||||||
@@ -201,7 +201,7 @@ function MyComponent() {
|
|||||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
@@ -313,7 +313,7 @@ const embeds: {
|
|||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
title: "Inline Embed",
|
title: "Inline Embed",
|
||||||
subtitle: "Loads your Cal scheduling page directly inline with your other website content",
|
subtitle: "Loads your event type directly inline with your other website content.",
|
||||||
type: "inline",
|
type: "inline",
|
||||||
illustration: (
|
illustration: (
|
||||||
<svg
|
<svg
|
||||||
@@ -798,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThemeOptions = [
|
const ThemeOptions = [
|
||||||
{ value: Theme.auto, label: "Auto Theme" },
|
{ value: Theme.auto, label: "Auto" },
|
||||||
{ value: Theme.dark, label: "Dark Theme" },
|
{ value: Theme.dark, label: "Dark Theme" },
|
||||||
{ value: Theme.light, label: "Light Theme" },
|
{ value: Theme.light, label: "Light Theme" },
|
||||||
];
|
];
|
||||||
@@ -815,11 +815,18 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation">
|
<DialogContent
|
||||||
|
ref={dialogContentRef}
|
||||||
|
className="rounded-lg p-0.5 sm:max-w-[80rem]"
|
||||||
|
enableOverflow
|
||||||
|
type="creation">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
||||||
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
<h3
|
||||||
|
className="mb-2.5 flex items-center text-xl font-semibold leading-5 text-gray-900"
|
||||||
|
id="modal-title">
|
||||||
<button
|
<button
|
||||||
|
className="h-6 w-6"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||||
}}>
|
}}>
|
||||||
@@ -827,55 +834,43 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</button>
|
</button>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
<h4 className="mb-6 text-sm font-normal text-gray-500">{embed.subtitle}</h4>
|
||||||
<div className="flex flex-col overflow-y-auto">
|
<div className="flex flex-col">
|
||||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isEmbedCustomizationOpen}
|
open={isEmbedCustomizationOpen}
|
||||||
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger
|
|
||||||
type="button"
|
|
||||||
className="flex w-full items-center text-base font-medium text-gray-900">
|
|
||||||
<div>
|
|
||||||
{embedType === "inline"
|
|
||||||
? "Inline Embed Customization"
|
|
||||||
: embedType === "floating-popup"
|
|
||||||
? "Floating Popup Customization"
|
|
||||||
: "Element Click Customization"}
|
|
||||||
</div>
|
|
||||||
<FiChevronRight
|
|
||||||
className={`${
|
|
||||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
|
||||||
/>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="text-sm">
|
<CollapsibleContent className="text-sm">
|
||||||
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
|
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
|
||||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||||
<div className="text-sm">Embed Window Sizing</div>
|
<div className="mb-[9px] text-sm">Window sizing</div>
|
||||||
<div className="justify-left flex items-center">
|
<div className="justify-left flex items-center !font-normal">
|
||||||
<TextField
|
<div className="mr-[9px]">
|
||||||
labelProps={{ className: "hidden" }}
|
<TextField
|
||||||
required
|
labelProps={{ className: "hidden" }}
|
||||||
value={previewState.inline.width}
|
className="focus:ring-offset-0"
|
||||||
onChange={(e) => {
|
required
|
||||||
setPreviewState((previewState) => {
|
value={previewState.inline.width}
|
||||||
const width = e.target.value || "100%";
|
onChange={(e) => {
|
||||||
|
setPreviewState((previewState) => {
|
||||||
|
const width = e.target.value || "100%";
|
||||||
|
|
||||||
|
return {
|
||||||
|
...previewState,
|
||||||
|
inline: {
|
||||||
|
...previewState.inline,
|
||||||
|
width,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
addOnLeading={<>W</>}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
return {
|
|
||||||
...previewState,
|
|
||||||
inline: {
|
|
||||||
...previewState.inline,
|
|
||||||
width,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
addOnLeading={<>W</>}
|
|
||||||
/>
|
|
||||||
<span className="p-2">×</span>
|
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
|
className="focus:ring-offset-0"
|
||||||
value={previewState.inline.height}
|
value={previewState.inline.height}
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -1007,22 +1002,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-4" />
|
<div className="font-medium">
|
||||||
<div className="mt-4 font-medium">
|
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isBookingCustomizationOpen}
|
open={isBookingCustomizationOpen}
|
||||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger className="flex w-full" type="button">
|
|
||||||
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
|
|
||||||
<FiChevronRight
|
|
||||||
className={`${
|
|
||||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
|
||||||
/>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="mt-6 text-sm">
|
<div className="mt-6 text-sm">
|
||||||
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
<div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
||||||
<Switch
|
<Switch
|
||||||
checked={previewState.hideEventTypeDetails}
|
checked={previewState.hideEventTypeDetails}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
@@ -1036,13 +1022,38 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
/>
|
/>
|
||||||
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
||||||
</div>
|
</div>
|
||||||
<Label className="">
|
{[
|
||||||
|
{ name: "brandColor", title: "Brand Color" },
|
||||||
|
// { name: "lightColor", title: "Light Color" },
|
||||||
|
// { name: "lighterColor", title: "Lighter Color" },
|
||||||
|
// { name: "lightestColor", title: "Lightest Color" },
|
||||||
|
// { name: "highlightColor", title: "Highlight Color" },
|
||||||
|
// { name: "medianColor", title: "Median Color" },
|
||||||
|
].map((palette) => (
|
||||||
|
<Label key={palette.name} className="mb-6">
|
||||||
|
<div className="mb-2">{palette.title}</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<ColorPicker
|
||||||
|
popoverAlign="start"
|
||||||
|
container={dialogContentRef?.current ?? undefined}
|
||||||
|
defaultValue="#000000"
|
||||||
|
onChange={(color) => {
|
||||||
|
addToPalette({
|
||||||
|
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
))}
|
||||||
|
<Label>
|
||||||
<div className="mb-2">Theme</div>
|
<div className="mb-2">Theme</div>
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
defaultValue={ThemeOptions[0]}
|
defaultValue={ThemeOptions[0]}
|
||||||
components={{
|
components={{
|
||||||
Control: ThemeSelectControl,
|
Control: ThemeSelectControl,
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
}}
|
}}
|
||||||
onChange={(option) => {
|
onChange={(option) => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
@@ -1058,30 +1069,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
options={ThemeOptions}
|
options={ThemeOptions}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
{[
|
|
||||||
{ name: "brandColor", title: "Brand Color" },
|
|
||||||
// { name: "lightColor", title: "Light Color" },
|
|
||||||
// { name: "lighterColor", title: "Lighter Color" },
|
|
||||||
// { name: "lightestColor", title: "Lightest Color" },
|
|
||||||
// { name: "highlightColor", title: "Highlight Color" },
|
|
||||||
// { name: "medianColor", title: "Median Color" },
|
|
||||||
].map((palette) => (
|
|
||||||
<Label key={palette.name} className="pb-4">
|
|
||||||
<div className="mb-2 pt-2">{palette.title}</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<ColorPicker
|
|
||||||
popoverAlign="start"
|
|
||||||
container={dialogContentRef?.current ?? undefined}
|
|
||||||
defaultValue="#000000"
|
|
||||||
onChange={(color) => {
|
|
||||||
addToPalette({
|
|
||||||
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { AdminRequired } from "components/ui/AdminRequired";
|
import { AdminRequired } from "components/ui/AdminRequired";
|
||||||
import noop from "lodash/noop";
|
import { noop } from "lodash";
|
||||||
import type { LinkProps } from "next/link";
|
import type { LinkProps } from "next/link";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import type { OptionProps } from "react-select";
|
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App } from "@calcom/types/App";
|
import {
|
||||||
import { Button, Select } from "@calcom/ui";
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownItem,
|
||||||
|
} from "@calcom/ui";
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
import { FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const { data } = optionProps;
|
|
||||||
return data.slug !== "add-new" ? (
|
|
||||||
<InstallAppButton
|
|
||||||
type={data.type}
|
|
||||||
render={(installProps) => {
|
|
||||||
return (
|
|
||||||
<Button {...installProps} className="flex w-full align-top" color="minimal">
|
|
||||||
{data.image && (
|
|
||||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
|
||||||
)}
|
|
||||||
<p className="text-left">{`${t("add")} ${data.label}`}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
|
|
||||||
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
|
||||||
<p>{t("install_new_calendar_app")}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||||
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
type: "new_other",
|
type: "new_other",
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Select
|
<Dropdown modal={false}>
|
||||||
name="additionalCalendar"
|
<DropdownMenuTrigger asChild>
|
||||||
placeholder={
|
<Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
||||||
<Button StartIcon={FiPlus} color="secondary">
|
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
</DropdownMenuTrigger>
|
||||||
options={options}
|
<DropdownMenuContent>
|
||||||
isSearchable={false}
|
{options.map((data) => (
|
||||||
isLoading={isLoading}
|
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||||
components={{ Option: ImageOption }}
|
{data.slug === "add-new" ? (
|
||||||
styles={{
|
<DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
|
||||||
menu: (defaultStyles) => ({
|
{t("install_new_calendar_app")}
|
||||||
...defaultStyles,
|
</DropdownItem>
|
||||||
"@media only screen and (max-width: 640px)": {
|
) : (
|
||||||
left: "0",
|
<InstallAppButton
|
||||||
},
|
type={data.type}
|
||||||
width: "max-content",
|
render={(installProps) => {
|
||||||
right: "0",
|
const props = { ...installProps } as any;
|
||||||
}),
|
return (
|
||||||
indicatorSeparator: (defaultStyles) => ({
|
<DropdownItem {...props} color="minimal" type="button">
|
||||||
...defaultStyles,
|
<span className="flex items-center gap-x-2">
|
||||||
display: "none",
|
{data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
|
||||||
}),
|
{`${t("add")} ${data.label}`}
|
||||||
control: (defaultStyles) => ({
|
</span>
|
||||||
...defaultStyles,
|
</DropdownItem>
|
||||||
padding: "0",
|
);
|
||||||
border: "0",
|
}}
|
||||||
borderRadius: "6px",
|
/>
|
||||||
minHeight: "auto",
|
)}
|
||||||
}),
|
</DropdownMenuItem>
|
||||||
dropdownIndicator: (defaultStyles) => ({
|
))}
|
||||||
...defaultStyles,
|
</DropdownMenuContent>
|
||||||
display: "none",
|
</Dropdown>
|
||||||
}),
|
|
||||||
valueContainer: (defaultStyles) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
padding: "0",
|
|
||||||
}),
|
|
||||||
placeholder: (defaultStyles) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
margin: "0",
|
|
||||||
}),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { DestinationCalendar } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onChange: (value: { externalId: string; integration: string }) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
hidePlaceholder?: boolean;
|
|
||||||
/** The external Id of the connected calendar */
|
|
||||||
value: string | undefined;
|
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DestinationCalendarSelector = ({
|
|
||||||
onChange,
|
|
||||||
isLoading,
|
|
||||||
value,
|
|
||||||
hidePlaceholder,
|
|
||||||
destinationCalendar,
|
|
||||||
}: Props): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
|
||||||
|
|
||||||
// Extra styles to show prefixed text in react-select
|
|
||||||
const content = (hidePlaceholder = false) => {
|
|
||||||
if (!hidePlaceholder) {
|
|
||||||
return {
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
":before": {
|
|
||||||
content: `'${t("select_destination_calendar")}:'`,
|
|
||||||
display: "block",
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selected = query.data?.connectedCalendars
|
|
||||||
.map((connected) => connected.calendars ?? [])
|
|
||||||
.flat()
|
|
||||||
.find((cal) => cal.externalId === value);
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
setSelectedOption({
|
|
||||||
value: `${selected.integration}:${selected.externalId}`,
|
|
||||||
label: `${selected.name} (${selected.externalId})` || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [query.data?.connectedCalendars, value]);
|
|
||||||
|
|
||||||
if (!query.data?.connectedCalendars.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const options =
|
|
||||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
|
||||||
key: selectedCalendar.credentialId,
|
|
||||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
|
||||||
options: (selectedCalendar.calendars ?? [])
|
|
||||||
.filter((cal) => cal.readOnly === false)
|
|
||||||
.map((cal) => ({
|
|
||||||
label: `${cal.name} (${cal.externalId})` || "",
|
|
||||||
value: `${cal.integration}:${cal.externalId}`,
|
|
||||||
})),
|
|
||||||
})) ?? [];
|
|
||||||
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
|
||||||
? `(${
|
|
||||||
destinationCalendar.externalId.length > 15
|
|
||||||
? destinationCalendar.externalId.substring(0, 15) + "..."
|
|
||||||
: destinationCalendar.externalId
|
|
||||||
})`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="relative flex items-center"
|
|
||||||
title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
|
||||||
<Select
|
|
||||||
name="primarySelectedCalendar"
|
|
||||||
placeholder={
|
|
||||||
!hidePlaceholder ? (
|
|
||||||
`${t("select_destination_calendar")}`
|
|
||||||
) : (
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "6px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className={classNames(
|
|
||||||
"block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
|
||||||
!hidePlaceholder && "font-medium"
|
|
||||||
)}
|
|
||||||
onChange={(option) => {
|
|
||||||
setSelectedOption(option);
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
|
||||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
integration,
|
|
||||||
externalId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
value={selectedOption}
|
|
||||||
components={{
|
|
||||||
IndicatorSeparator: () => null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DestinationCalendarSelector;
|
|
||||||
@@ -8,54 +8,61 @@ import { FiLink } from "@calcom/ui/components/icon";
|
|||||||
|
|
||||||
import type { Props } from "./pages/AvailabilityPage";
|
import type { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
|
const excludeNullValues = (value: unknown) => !!value;
|
||||||
|
|
||||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return locations.length ? (
|
const renderLocations = locations.map((location, index) => {
|
||||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
const eventLocationType = getEventLocationType(location.type);
|
||||||
{locations.map((location, index) => {
|
if (!eventLocationType) {
|
||||||
const eventLocationType = getEventLocationType(location.type);
|
// It's possible that the location app got uninstalled
|
||||||
if (!eventLocationType) {
|
return null;
|
||||||
// It's possible that the location app got uninstalled
|
}
|
||||||
return null;
|
if (eventLocationType.variable === "hostDefault") {
|
||||||
}
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const translateAbleKeys = [
|
const translateAbleKeys = [
|
||||||
"attendee_in_person",
|
"attendee_in_person",
|
||||||
"in_person",
|
"in_person",
|
||||||
"attendee_phone_number",
|
"attendee_phone_number",
|
||||||
"link_meeting",
|
"link_meeting",
|
||||||
"organizer_phone_number",
|
"organizer_phone_number",
|
||||||
];
|
];
|
||||||
|
|
||||||
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
||||||
|
const translatedLocation = location.type.startsWith("integrations:")
|
||||||
|
? eventLocationType.label
|
||||||
|
: translateAbleKeys.includes(locationKey)
|
||||||
|
? t(locationKey)
|
||||||
|
: locationKey;
|
||||||
|
|
||||||
const translatedLocation = location.type.startsWith("integrations:")
|
return (
|
||||||
? eventLocationType.label
|
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||||
: translateAbleKeys.includes(locationKey)
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
? t(locationKey)
|
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||||
: locationKey;
|
) : (
|
||||||
|
<img
|
||||||
return (
|
src={eventLocationType.iconUrl}
|
||||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
className={classNames(
|
||||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||||
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
src={eventLocationType.iconUrl}
|
|
||||||
className={classNames(
|
|
||||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
|
||||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
|
||||||
)}
|
|
||||||
alt={`${eventLocationType.label} icon`}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
<Tooltip content={translatedLocation}>
|
alt={`${eventLocationType.label} icon`}
|
||||||
<p className="line-clamp-1">{translatedLocation}</p>
|
/>
|
||||||
</Tooltip>
|
)}
|
||||||
</div>
|
<Tooltip content={translatedLocation}>
|
||||||
);
|
<p className="line-clamp-1">{translatedLocation}</p>
|
||||||
})}
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
|
||||||
|
return filteredLocations.length ? (
|
||||||
|
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
||||||
|
{filteredLocations}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { useState } from "react";
|
|||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||||
|
import "@calcom/dayjs/locales";
|
||||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { formatTime } from "@calcom/lib/date-fns";
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
{t("nevermind")}
|
{t("nevermind")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="flex justify-center"
|
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast flex justify-center"
|
||||||
data-testid="cancel"
|
data-testid="cancel"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|||||||
@@ -133,10 +133,7 @@ export const SlotPicker = ({
|
|||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
usernameList: users,
|
usernameList: users,
|
||||||
startTime:
|
startTime: dayjs().startOf("day"),
|
||||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
|
||||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
|
||||||
: browsingDate?.startOf("month"),
|
|
||||||
endTime: browsingDate?.endOf("month"),
|
endTime: browsingDate?.endOf("month"),
|
||||||
timeZone,
|
timeZone,
|
||||||
duration,
|
duration,
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
|
||||||
import type { ITimezoneOption } from "@calcom/ui";
|
import type { ITimezoneOption } from "@calcom/ui";
|
||||||
import { TimezoneSelect } from "@calcom/ui";
|
import { TimezoneSelect } from "@calcom/ui";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
|
||||||
|
|
||||||
import { timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -15,8 +12,6 @@ type Props = {
|
|||||||
|
|
||||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const query = useMeQuery();
|
|
||||||
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedTimeZone(timeZone());
|
setSelectedTimeZone(timeZone());
|
||||||
@@ -28,59 +23,16 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
|||||||
}
|
}
|
||||||
}, [selectedTimeZone, onSelectTimeZone]);
|
}, [selectedTimeZone, onSelectTimeZone]);
|
||||||
|
|
||||||
const customStyles = {
|
|
||||||
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
|
||||||
padding: "10px 12px !important",
|
|
||||||
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
|
||||||
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
control: () => ({
|
|
||||||
display: "flex",
|
|
||||||
cursor: "pointer",
|
|
||||||
backgroundColor: "transparent !important",
|
|
||||||
minWidth: "5rem",
|
|
||||||
height: "24px",
|
|
||||||
minHeight: "24px !important",
|
|
||||||
boxShadow: "none !important",
|
|
||||||
}),
|
|
||||||
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
|
||||||
...provided,
|
|
||||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: "#a5a5a5 !important",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
menu: (provided: object) => ({
|
|
||||||
...provided,
|
|
||||||
minWidth: "20rem",
|
|
||||||
margin: "8px 0 0 -20px",
|
|
||||||
}),
|
|
||||||
valueContainer: (provided: object) => ({
|
|
||||||
...provided,
|
|
||||||
padding: "0 0 0 6px",
|
|
||||||
}),
|
|
||||||
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
|
||||||
...provided,
|
|
||||||
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
|
|
||||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
|
||||||
marginLeft: 4,
|
|
||||||
padding: 0,
|
|
||||||
...(userTheme === "dark" && {
|
|
||||||
color: "#80868B",
|
|
||||||
}),
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
return !!selectedTimeZone ? (
|
return !!selectedTimeZone ? (
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
|
classNames={{
|
||||||
|
singleValue: () => "dark:text-darkgray-600 text-gray-600",
|
||||||
|
menu: () => "!w-64 max-w-[90vw]",
|
||||||
|
}}
|
||||||
|
variant="minimal"
|
||||||
value={selectedTimeZone}
|
value={selectedTimeZone}
|
||||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||||
className="flex h-6 text-sm font-medium"
|
|
||||||
styles={customStyles}
|
|
||||||
/>
|
/>
|
||||||
) : null;
|
) : null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,8 +17,8 @@ export function TimezoneDropdown({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
||||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -205,6 +205,7 @@ const routerQuerySchema = z
|
|||||||
const BookingPage = ({
|
const BookingPage = ({
|
||||||
eventType,
|
eventType,
|
||||||
booking,
|
booking,
|
||||||
|
currentSlotBooking,
|
||||||
profile,
|
profile,
|
||||||
isDynamicGroupBooking,
|
isDynamicGroupBooking,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
@@ -287,6 +288,7 @@ const BookingPage = ({
|
|||||||
return router.push({
|
return router.push({
|
||||||
pathname: `/booking/${uid}`,
|
pathname: `/booking/${uid}`,
|
||||||
query: {
|
query: {
|
||||||
|
isSuccessBookingPage: true,
|
||||||
allRemainingBookings: true,
|
allRemainingBookings: true,
|
||||||
email: bookingForm.getValues("responses.email"),
|
email: bookingForm.getValues("responses.email"),
|
||||||
eventTypeSlug: eventType.slug,
|
eventTypeSlug: eventType.slug,
|
||||||
@@ -583,23 +585,27 @@ const BookingPage = ({
|
|||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<FiUser
|
<FiUser
|
||||||
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
? "text-rose-600"
|
? "text-rose-600"
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
: currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
? "text-yellow-500"
|
? "text-yellow-500"
|
||||||
: "text-bookinghighlight"
|
: "text-bookinghighlight"
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<p
|
<p
|
||||||
className={`${
|
className={`${
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
? "text-rose-600"
|
? "text-rose-600"
|
||||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
: currentSlotBooking &&
|
||||||
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
? "text-yellow-500"
|
? "text-yellow-500"
|
||||||
: "text-bookinghighlight"
|
: "text-bookinghighlight"
|
||||||
} mb-2 font-medium`}>
|
} mb-2 font-medium`}>
|
||||||
{booking
|
{currentSlotBooking
|
||||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
|
||||||
: eventType.seatsPerTimeSlot}{" "}
|
: eventType.seatsPerTimeSlot}{" "}
|
||||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||||
</p>
|
</p>
|
||||||
@@ -631,6 +637,7 @@ const BookingPage = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast"
|
||||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
||||||
loading={mutation.isLoading || recurringMutation.isLoading}>
|
loading={mutation.isLoading || recurringMutation.isLoading}>
|
||||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface ISetLocationDialog {
|
|||||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
||||||
selection?: LocationOption;
|
selection?: LocationOption;
|
||||||
booking?: BookingItem;
|
booking?: BookingItem;
|
||||||
|
isTeamEvent?: boolean;
|
||||||
defaultValues?: LocationObject[];
|
defaultValues?: LocationObject[];
|
||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -74,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
saveLocation,
|
saveLocation,
|
||||||
selection,
|
selection,
|
||||||
booking,
|
booking,
|
||||||
|
isTeamEvent,
|
||||||
setShowLocationModal,
|
setShowLocationModal,
|
||||||
isOpenDialog,
|
isOpenDialog,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -290,7 +292,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
query={locationsQuery}
|
query={locationsQuery}
|
||||||
success={({ data }) => {
|
success={({ data }) => {
|
||||||
if (!data.length) return null;
|
if (!data.length) return null;
|
||||||
const locationOptions = [...data];
|
const locationOptions = [...data].filter((option) => {
|
||||||
|
return !isTeamEvent ? option.label !== "Conferencing" : true;
|
||||||
|
});
|
||||||
if (booking) {
|
if (booking) {
|
||||||
locationOptions.map((location) =>
|
locationOptions.map((location) =>
|
||||||
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
|
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import type { OptionProps, SingleValueProps } from "react-select";
|
|||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import { NewScheduleButton } from "@calcom/features/schedules";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
||||||
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
@@ -52,23 +54,24 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
|||||||
|
|
||||||
const AvailabilitySelect = ({
|
const AvailabilitySelect = ({
|
||||||
className = "",
|
className = "",
|
||||||
|
isLoading,
|
||||||
|
schedules,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
isLoading: boolean;
|
||||||
|
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
|
||||||
onBlur: () => void;
|
onBlur: () => void;
|
||||||
onChange: (value: AvailabilityOption | null) => void;
|
onChange: (value: AvailabilityOption | null) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <SelectSkeletonLoader />;
|
return <SelectSkeletonLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const schedules = data?.schedules || [];
|
|
||||||
|
|
||||||
const options = schedules.map((schedule) => ({
|
const options = schedules.map((schedule) => ({
|
||||||
value: schedule.id,
|
value: schedule.id,
|
||||||
label: schedule.name,
|
label: schedule.name,
|
||||||
@@ -107,6 +110,7 @@ const EventTypeScheduleDetails = () => {
|
|||||||
const { watch } = useFormContext<FormValues>();
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
|
||||||
const scheduleId = watch("schedule");
|
const scheduleId = watch("schedule");
|
||||||
|
|
||||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
||||||
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
||||||
@@ -174,6 +178,19 @@ const EventTypeScheduleDetails = () => {
|
|||||||
|
|
||||||
const EventTypeSchedule = () => {
|
const EventTypeSchedule = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const { data: schedules, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||||
|
|
||||||
|
if (!schedules?.schedules.length && !isLoading)
|
||||||
|
return (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={FiClock}
|
||||||
|
headline={t("new_schedule_heading")}
|
||||||
|
description={t("new_schedule_description")}
|
||||||
|
buttonRaw={<NewScheduleButton fromEventType />}
|
||||||
|
border={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
@@ -183,14 +200,18 @@ const EventTypeSchedule = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="schedule"
|
name="schedule"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<AvailabilitySelect
|
<>
|
||||||
value={field.value}
|
<AvailabilitySelect
|
||||||
onBlur={field.onBlur}
|
value={field.value}
|
||||||
name={field.name}
|
onBlur={field.onBlur}
|
||||||
onChange={(selected) => {
|
name={field.name}
|
||||||
field.onChange(selected?.value || null);
|
schedules={schedules?.schedules || []}
|
||||||
}}
|
isLoading={isLoading}
|
||||||
/>
|
onChange={(selected) => {
|
||||||
|
field.onChange(selected?.value || null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import type { EventNameObjectType } from "@calcom/core/event";
|
|||||||
import { getEventName } from "@calcom/core/event";
|
import { getEventName } from "@calcom/core/event";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
import { classNames } from "@calcom/lib";
|
||||||
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Badge, Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
||||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
@@ -183,9 +184,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
setRedirectUrlVisible(e);
|
setRedirectUrlVisible(e);
|
||||||
onChange(e ? value : "");
|
onChange(e ? value : "");
|
||||||
}}>
|
}}>
|
||||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
|
||||||
<div className="lg:-ml-2">
|
<div className="lg:-ml-2 lg:-mb-2">
|
||||||
<TextField
|
<TextField
|
||||||
|
className="w-full"
|
||||||
label={t("redirect_success_booking")}
|
label={t("redirect_success_booking")}
|
||||||
labelSrOnly
|
labelSrOnly
|
||||||
placeholder={t("external_redirect_url")}
|
placeholder={t("external_redirect_url")}
|
||||||
@@ -194,21 +196,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
defaultValue={eventType.successRedirectUrl || ""}
|
defaultValue={eventType.successRedirectUrl || ""}
|
||||||
{...formMethods.register("successRedirectUrl")}
|
{...formMethods.register("successRedirectUrl")}
|
||||||
/>
|
/>
|
||||||
<div className="mt-2 flex">
|
<div
|
||||||
<Checkbox
|
className={classNames(
|
||||||
description={t("disable_success_page")}
|
"p-1 text-sm text-orange-600",
|
||||||
// Disable if it's not Self Hosted or if the redirect url is not set
|
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
|
||||||
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
|
)}>
|
||||||
{...formMethods.register("metadata.disableSuccessPage")}
|
{t("redirect_url_warning")}
|
||||||
/>
|
|
||||||
{/*TODO: Extract it out into a component when used more than once*/}
|
|
||||||
{!IS_SELF_HOSTED && (
|
|
||||||
<Link href="https://cal.com/pricing" target="_blank">
|
|
||||||
<Badge variant="orange" className="ml-2">
|
|
||||||
Platform Only
|
|
||||||
</Badge>
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
label={t("minimum_booking_notice")}
|
label={t("minimum_booking_notice")}
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="0"
|
placeholder="0"
|
||||||
|
min={0}
|
||||||
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
||||||
/>
|
/>
|
||||||
<input type="hidden" ref={ref} {...passThroughProps} />
|
<input type="hidden" ref={ref} {...passThroughProps} />
|
||||||
@@ -425,6 +426,7 @@ const IntervalLimitItem = ({
|
|||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
||||||
|
className="mb-0"
|
||||||
placeholder={`${value}`}
|
placeholder={`${value}`}
|
||||||
min={step}
|
min={step}
|
||||||
step={step}
|
step={step}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import type { MultiValue } from "react-select";
|
import type { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -16,7 +16,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { slugify } from "@calcom/lib/slugify";
|
import { slugify } from "@calcom/lib/slugify";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
import {
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
SettingsToggle,
|
||||||
|
Skeleton,
|
||||||
|
TextField,
|
||||||
|
Editor,
|
||||||
|
SkeletonContainer,
|
||||||
|
SkeletonText,
|
||||||
|
} from "@calcom/ui";
|
||||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
@@ -35,14 +45,31 @@ const getLocationFromType = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
interface DescriptionEditorProps {
|
||||||
for (const locationType of options) {
|
description?: string | null;
|
||||||
for (const location of locationType.options) {
|
}
|
||||||
if (location.value === type && location.disabled === false) {
|
|
||||||
return location;
|
const DescriptionEditor = (props: DescriptionEditorProps) => {
|
||||||
}
|
const formMethods = useFormContext<FormValues>();
|
||||||
}
|
const [mounted, setIsMounted] = useState(false);
|
||||||
}
|
const { t } = useLocale();
|
||||||
|
const { description } = props;
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mounted ? (
|
||||||
|
<Editor
|
||||||
|
getText={() => md.render(formMethods.getValues("description") || description || "")}
|
||||||
|
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||||
|
excludedToolbarItems={["blockType"]}
|
||||||
|
placeholder={t("quick_video_meeting")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SkeletonContainer>
|
||||||
|
<SkeletonText className="block h-24 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventSetupTab = (
|
export const EventSetupTab = (
|
||||||
@@ -53,12 +80,16 @@ export const EventSetupTab = (
|
|||||||
) => {
|
) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { eventType, locationOptions, team, destinationCalendar } = props;
|
const { eventType, team, destinationCalendar } = props;
|
||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
||||||
|
|
||||||
|
const locationOptions = props.locationOptions.filter((option) => {
|
||||||
|
return !team ? option.label !== "Conferencing" : true;
|
||||||
|
});
|
||||||
|
|
||||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||||
value: mins,
|
value: mins,
|
||||||
label: t("multiple_duration_mins", { count: mins }),
|
label: t("multiple_duration_mins", { count: mins }),
|
||||||
@@ -198,14 +229,6 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<li
|
<li
|
||||||
onClick={() => {
|
|
||||||
locationFormMethods.setValue("locationType", location.type);
|
|
||||||
locationFormMethods.unregister("locationLink");
|
|
||||||
locationFormMethods.unregister("locationAddress");
|
|
||||||
locationFormMethods.unregister("locationPhoneNumber");
|
|
||||||
setEditingLocationType(location.type);
|
|
||||||
openLocationModal(location.type);
|
|
||||||
}}
|
|
||||||
key={`${location.type}${index}`}
|
key={`${location.type}${index}`}
|
||||||
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
|
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2 hover:cursor-pointer">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
@@ -287,12 +310,7 @@ export const EventSetupTab = (
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Label>{t("description")}</Label>
|
<Label>{t("description")}</Label>
|
||||||
<Editor
|
<DescriptionEditor description={eventType?.description} />
|
||||||
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
|
|
||||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
|
||||||
excludedToolbarItems={["blockType"]}
|
|
||||||
placeholder={t("quick_video_meeting")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
@@ -409,6 +427,7 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
||||||
<EditLocationDialog
|
<EditLocationDialog
|
||||||
|
isTeamEvent={!!team}
|
||||||
isOpenDialog={showLocationModal}
|
isOpenDialog={showLocationModal}
|
||||||
setShowLocationModal={setShowLocationModal}
|
setShowLocationModal={setShowLocationModal}
|
||||||
saveLocation={saveLocation}
|
saveLocation={saveLocation}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export type EventTypeDescriptionSafeProps = {
|
export type EventTypeDescriptionSafeProps = {
|
||||||
eventType: { description: string | null };
|
eventType: { description: string | null; descriptionAsSafeHTML: string | null };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
||||||
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
||||||
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
|
if (eventType.description)
|
||||||
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
|
props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
|
||||||
return <div {...props} />;
|
return <div {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ function EventTypeSingleLayout({
|
|||||||
tooltip={t("embed")}
|
tooltip={t("embed")}
|
||||||
/>
|
/>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="destructive"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
StartIcon={FiTrash}
|
StartIcon={FiTrash}
|
||||||
tooltip={t("delete")}
|
tooltip={t("delete")}
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { RouterInputs } from "@calcom/trpc/react";
|
import type { RouterInputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
|
||||||
|
|
||||||
interface ICreateEventsOnCalendarSelectProps {
|
interface ICreateEventsOnCalendarSelectProps {
|
||||||
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
|
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,7 +75,9 @@ export function CalendarSwitch(props: {
|
|||||||
/>
|
/>
|
||||||
{props.defaultSelected && (
|
{props.defaultSelected && (
|
||||||
<Badge variant="gray">
|
<Badge variant="gray">
|
||||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
<div className="flex">
|
||||||
|
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||||
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import Link from "next/link";
|
|||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
|
||||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
type TeamType = NonNullable<TeamWithMembers>;
|
type TeamType = NonNullable<TeamWithMembers>;
|
||||||
type MembersType = TeamType["members"];
|
type MembersType = TeamType["members"];
|
||||||
type MemberType = MembersType[number];
|
type MemberType = MembersType[number] & { safeBio: string | null };
|
||||||
|
|
||||||
|
type TeamTypeWithSafeHtml = Omit<TeamType, "members"> & { members: MemberType[] };
|
||||||
|
|
||||||
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -30,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: member.safeBio || "" }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -43,7 +44,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Members = ({ members, teamName }: { members: MembersType; teamName: string | null }) => {
|
const Members = ({ members, teamName }: { members: MemberType[]; teamName: string | null }) => {
|
||||||
if (!members || members.length === 0) {
|
if (!members || members.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Team = ({ team }: { team: TeamType }) => {
|
const Team = ({ team }: { team: TeamTypeWithSafeHtml }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Members members={team.members} teamName={team.name} />
|
<Members members={team.members} teamName={team.name} />
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import type { ComponentProps, ReactNode } from "react";
|
|||||||
|
|
||||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||||
|
import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
||||||
|
import { useFlags } from "@calcom/features/flags/hooks";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { MetaProvider } from "@calcom/ui";
|
import { MetaProvider } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -58,6 +60,11 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
|||||||
return <I18nextAdapter {...passedProps} />;
|
return <I18nextAdapter {...passedProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||||
|
const flags = useFlags();
|
||||||
|
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||||
|
}
|
||||||
|
|
||||||
const AppProviders = (props: AppPropsWithChildren) => {
|
const AppProviders = (props: AppPropsWithChildren) => {
|
||||||
const session = trpc.viewer.public.session.useQuery().data;
|
const session = trpc.viewer.public.session.useQuery().data;
|
||||||
// No need to have intercom on public pages - Good for Page Performance
|
// No need to have intercom on public pages - Good for Page Performance
|
||||||
@@ -85,7 +92,9 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
forcedTheme={forcedTheme}
|
forcedTheme={forcedTheme}
|
||||||
attribute="class">
|
attribute="class">
|
||||||
<MetaProvider>{props.children}</MetaProvider>
|
<FeatureFlagsProvider>
|
||||||
|
<MetaProvider>{props.children}</MetaProvider>
|
||||||
|
</FeatureFlagsProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
</CustomI18nextProvider>
|
</CustomI18nextProvider>
|
||||||
|
|||||||
@@ -99,6 +99,14 @@ const nextConfig = {
|
|||||||
skipDefaultConversion: true,
|
skipDefaultConversion: true,
|
||||||
preventFullImport: true,
|
preventFullImport: true,
|
||||||
},
|
},
|
||||||
|
"@calcom/features/insights/components": {
|
||||||
|
transform: "@calcom/features/insights/components/{{member}}",
|
||||||
|
skipDefaultConversion: true,
|
||||||
|
preventFullImport: true,
|
||||||
|
},
|
||||||
|
lodash: {
|
||||||
|
transform: "lodash/{{member}}",
|
||||||
|
},
|
||||||
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
||||||
// "@calcom/ui": {
|
// "@calcom/ui": {
|
||||||
// transform: "@calcom/ui/components/{{member}}",
|
// transform: "@calcom/ui/components/{{member}}",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.7.3",
|
"version": "2.7.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"dx": "yarn dev",
|
"dx": "yarn dev",
|
||||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint . --ignore-path .gitignore",
|
"lint": "eslint . --ignore-path .gitignore",
|
||||||
@@ -60,10 +61,11 @@
|
|||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
|
"@tremor/react": "^2.0.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
"@vercel/edge-config": "^0.1.1",
|
"@vercel/edge-config": "^0.1.1",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
"@vercel/og": "^0.0.21",
|
"@vercel/og": "^0.5.0",
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
@@ -118,6 +120,7 @@
|
|||||||
"react-window": "^1.8.7",
|
"react-window": "^1.8.7",
|
||||||
"remark": "^14.0.2",
|
"remark": "^14.0.2",
|
||||||
"rrule": "^2.7.1",
|
"rrule": "^2.7.1",
|
||||||
|
"sanitize-html": "^2.10.0",
|
||||||
"schema-dts": "^1.1.0",
|
"schema-dts": "^1.1.0",
|
||||||
"short-uuid": "^4.2.0",
|
"short-uuid": "^4.2.0",
|
||||||
"strip-markdown": "^5.0.0",
|
"strip-markdown": "^5.0.0",
|
||||||
@@ -155,6 +158,7 @@
|
|||||||
"@types/react-phone-number-input": "^3.0.14",
|
"@types/react-phone-number-input": "^3.0.14",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@types/stripe": "^8.0.417",
|
"@types/stripe": "^8.0.417",
|
||||||
"@types/uuid": "8.3.1",
|
"@types/uuid": "8.3.1",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
import { FiFileText, FiCheck, FiBookOpen, FiChevronRight } from "@calcom/ui/components/icon";
|
import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
@@ -40,6 +40,43 @@ export default function Custom404() {
|
|||||||
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
||||||
const isSignup = router.asPath.startsWith("/signup");
|
const isSignup = router.asPath.startsWith("/signup");
|
||||||
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
|
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
|
||||||
|
/**
|
||||||
|
* If we're on 404 and the route is insights it means it is disabled
|
||||||
|
* TODO: Abstract this for all disabled features
|
||||||
|
**/
|
||||||
|
const isInsights = router.asPath.startsWith("/insights");
|
||||||
|
if (isInsights) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HeadSeo
|
||||||
|
title="Feature is currently disabled"
|
||||||
|
description={t("404_page_not_found")}
|
||||||
|
nextSeoProps={{
|
||||||
|
nofollow: true,
|
||||||
|
noindex: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="min-h-screen bg-white px-4" data-testid="404-page">
|
||||||
|
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
|
||||||
|
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
|
||||||
|
Feature is currently disabled
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="mt-12">
|
||||||
|
<div className="mt-8">
|
||||||
|
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
|
||||||
|
{t("or_go_back_home")}
|
||||||
|
<span aria-hidden="true"> →</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -62,7 +99,7 @@ export default function Custom404() {
|
|||||||
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
|
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
|
||||||
{t("signup_requires")}
|
{t("signup_requires")}
|
||||||
</h1>
|
</h1>
|
||||||
<p className="mt-4">{t("signup_requires_description", { companyName: COMPANY_NAME })}</p>
|
<p className="mt-4">{t("signup_requires_description", { companyName: "Cal.com" })}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-12">
|
<div className="mt-12">
|
||||||
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
|
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">
|
||||||
@@ -99,7 +136,7 @@ export default function Custom404() {
|
|||||||
<ul role="list" className="divide-y divide-gray-200 border-gray-200">
|
<ul role="list" className="divide-y divide-gray-200 border-gray-200">
|
||||||
<li className="px-4 py-2">
|
<li className="px-4 py-2">
|
||||||
<Link
|
<Link
|
||||||
href={`${DEVELOPER_DOCS}/self-hosting/installation`}
|
href="https://cal.com/self-hosting/installation"
|
||||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
||||||
@@ -168,7 +205,7 @@ export default function Custom404() {
|
|||||||
</ul>
|
</ul>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<Link
|
<Link
|
||||||
href={`${WEBSITE_URL}/enterprise`}
|
href="https://cal.com/sales"
|
||||||
className="text-base font-medium text-black hover:text-gray-500">
|
className="text-base font-medium text-black hover:text-gray-500">
|
||||||
{t("contact_sales")}
|
{t("contact_sales")}
|
||||||
<span aria-hidden="true"> →</span>
|
<span aria-hidden="true"> →</span>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import defaultEvents, {
|
|||||||
} from "@calcom/lib/defaultEvents";
|
} from "@calcom/lib/defaultEvents";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
@@ -147,7 +147,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -343,6 +343,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const eventTypes = eventTypesRaw.map((eventType) => ({
|
const eventTypes = eventTypesRaw.map((eventType) => ({
|
||||||
...eventType,
|
...eventType,
|
||||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const isSingleUser = users.length === 1;
|
const isSingleUser = users.length === 1;
|
||||||
@@ -352,9 +353,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
users,
|
users,
|
||||||
|
safeBio,
|
||||||
profile,
|
profile,
|
||||||
user: {
|
user: {
|
||||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
|||||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { addListFormatting } from "@calcom/lib/markdownIt";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import type { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
@@ -59,7 +59,6 @@ Type.isThemeSupported = true;
|
|||||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
async function getUserPageProps(context: GetStaticPropsContext) {
|
||||||
// load server side dependencies
|
// load server side dependencies
|
||||||
const MarkdownIt = await import("markdown-it").then((mod) => mod.default);
|
|
||||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||||
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
||||||
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
||||||
@@ -124,9 +123,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
|
||||||
|
|
||||||
if (!user || !user.eventTypes.length) return { notFound: true };
|
if (!user || !user.eventTypes.length) return { notFound: true };
|
||||||
|
|
||||||
const [eventType]: ((typeof user.eventTypes)[number] & {
|
const [eventType]: ((typeof user.eventTypes)[number] & {
|
||||||
@@ -153,7 +149,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||||
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
||||||
locations: privacyFilteredLocations(locations),
|
locations: privacyFilteredLocations(locations),
|
||||||
descriptionAsSafeHTML: eventType.description ? addListFormatting(md.render(eventType.description)) : null,
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
});
|
});
|
||||||
// Check if the user you are logging into has any active teams or premium user name
|
// Check if the user you are logging into has any active teams or premium user name
|
||||||
const hasActiveTeam =
|
const hasActiveTeam =
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ import {
|
|||||||
getGroupName,
|
getGroupName,
|
||||||
getUsernameList,
|
getUsernameList,
|
||||||
} from "@calcom/lib/defaultEvents";
|
} from "@calcom/lib/defaultEvents";
|
||||||
|
import getBooking from "@calcom/lib/getBooking";
|
||||||
|
import type { GetBookingType } from "@calcom/lib/getBooking";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
||||||
import {
|
import {
|
||||||
customInputSchema,
|
customInputSchema,
|
||||||
@@ -21,8 +24,6 @@ import {
|
|||||||
userMetadata as userMetadataSchema,
|
userMetadata as userMetadataSchema,
|
||||||
} from "@calcom/prisma/zod-utils";
|
} from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import type { GetBookingType } from "@lib/getBooking";
|
|
||||||
import getBooking from "@lib/getBooking";
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import BookingPage from "@components/booking/pages/BookingPage";
|
import BookingPage from "@components/booking/pages/BookingPage";
|
||||||
@@ -189,6 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
slug: u.username,
|
slug: u.username,
|
||||||
theme: u.theme,
|
theme: u.theme,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
};
|
};
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
@@ -217,7 +219,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
rescheduleUid = bookingSeat.booking.uid;
|
rescheduleUid = bookingSeat.booking.uid;
|
||||||
attendeeEmail = bookingSeat.attendee.email;
|
attendeeEmail = bookingSeat.attendee.email;
|
||||||
}
|
}
|
||||||
} else if (eventTypeRaw.seatsPerTimeSlot && query.duration && query.date) {
|
}
|
||||||
|
|
||||||
|
if (query.duration) {
|
||||||
// If it's not reschedule but event Type has seats we should obtain
|
// If it's not reschedule but event Type has seats we should obtain
|
||||||
// the bookingUid regardless and use it to get the booking
|
// the bookingUid regardless and use it to get the booking
|
||||||
const currentSeats = await prisma.booking.findFirst({
|
const currentSeats = await prisma.booking.findFirst({
|
||||||
@@ -237,11 +241,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
|
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
|
||||||
booking = await getBooking(
|
booking = await getBooking(prisma, rescheduleUid || query.bookingUid || bookingUidWithSeats || "");
|
||||||
prisma,
|
|
||||||
rescheduleUid || query.bookingUid || bookingUidWithSeats || "",
|
|
||||||
eventTypeObject.bookingFields
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
|
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
|
||||||
@@ -285,12 +285,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
: eventType.recurringEvent.count)) ||
|
: eventType.recurringEvent.count)) ||
|
||||||
null;
|
null;
|
||||||
|
|
||||||
|
const currentSlotBooking = await getBooking(prisma, bookingUidWithSeats || "");
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
away: user.away,
|
away: user.away,
|
||||||
profile,
|
profile,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
booking,
|
booking,
|
||||||
|
currentSlotBooking: currentSlotBooking,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isDynamicGroupBooking,
|
isDynamicGroupBooking,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
|
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
|
||||||
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
@@ -48,8 +49,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
eventType: {
|
eventType: {
|
||||||
select: {
|
select: {
|
||||||
recurringEvent: true,
|
recurringEvent: true,
|
||||||
|
bookingFields: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
responses: true,
|
||||||
uid: true,
|
uid: true,
|
||||||
destinationCalendar: true,
|
destinationCalendar: true,
|
||||||
},
|
},
|
||||||
@@ -96,6 +99,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
title: booking.title,
|
title: booking.title,
|
||||||
description: booking.description || undefined,
|
description: booking.description || undefined,
|
||||||
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
||||||
|
...getCalEventResponses({
|
||||||
|
bookingFields: booking.eventType?.bookingFields ?? null,
|
||||||
|
booking,
|
||||||
|
}),
|
||||||
location: booking.location ?? "",
|
location: booking.location ?? "",
|
||||||
startTime: booking.startTime.toISOString(),
|
startTime: booking.startTime.toISOString(),
|
||||||
endTime: booking.endTime.toISOString(),
|
endTime: booking.endTime.toISOString(),
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
page: z.coerce.number().min(0).optional().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||||
@@ -11,10 +19,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
* TODO:
|
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
|
||||||
* Remove this endpoint
|
let { page: pageNumber } = querySchema.parse(req.query);
|
||||||
*/
|
|
||||||
|
while (true) {
|
||||||
|
const teams = await prisma.team.findMany({
|
||||||
|
where: {
|
||||||
|
slug: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
skip: pageNumber * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (teams.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const team of teams) {
|
||||||
|
await updateQuantitySubscriptionFromStripe(team.id);
|
||||||
|
await delay(100); // Adjust the delay as needed to avoid rate limiting
|
||||||
|
}
|
||||||
|
|
||||||
|
pageNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
ogConfig
|
ogConfig
|
||||||
) as { body: Buffer };
|
) as { body: Buffer };
|
||||||
|
|
||||||
return new Response(img.body, { status: 200 });
|
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||||
}
|
}
|
||||||
case "app": {
|
case "app": {
|
||||||
const { name, description, slug } = appSchema.parse({
|
const { name, description, slug } = appSchema.parse({
|
||||||
@@ -101,7 +101,7 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
body: Buffer;
|
body: Buffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Response(img.body, { status: 200 });
|
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||||
}
|
}
|
||||||
|
|
||||||
case "generic": {
|
case "generic": {
|
||||||
@@ -115,7 +115,7 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
body: Buffer;
|
body: Buffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Response(img.body, { status: 200 });
|
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { FiSearch } from "@calcom/ui/components/icon";
|
|||||||
|
|
||||||
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const tabs: HorizontalTabItemProps[] = [
|
const tabs: HorizontalTabItemProps[] = [
|
||||||
{
|
{
|
||||||
@@ -88,7 +88,7 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
|
|||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { req, res } = context;
|
const { req, res } = context;
|
||||||
|
|
||||||
const ssg = await ssgInit(context);
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
const session = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
return b.count - a.count;
|
return b.count - a.count;
|
||||||
}),
|
}),
|
||||||
appStore,
|
appStore,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ResetPasswordRequest } from "@prisma/client";
|
import type { ResetPasswordRequest } from "@prisma/client";
|
||||||
import debounce from "lodash/debounce";
|
import { debounce } from "lodash";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { getCsrfToken } from "next-auth/react";
|
import { getCsrfToken } from "next-auth/react";
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import debounce from "lodash/debounce";
|
import { debounce } from "lodash";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import { getCsrfToken } from "next-auth/react";
|
import { getCsrfToken } from "next-auth/react";
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ export default function Availability() {
|
|||||||
data: { schedule: scheduleId },
|
data: { schedule: scheduleId },
|
||||||
} = useTypedQuery(querySchema);
|
} = useTypedQuery(querySchema);
|
||||||
|
|
||||||
|
const { fromEventType } = router.query;
|
||||||
const { timeFormat } = me.data || { timeFormat: null };
|
const { timeFormat } = me.data || { timeFormat: null };
|
||||||
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
{ scheduleId },
|
{ scheduleId },
|
||||||
@@ -153,7 +154,7 @@ export default function Availability() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
backPath="/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
|
||||||
@@ -200,7 +201,14 @@ export default function Availability() {
|
|||||||
<VerticalDivider />
|
<VerticalDivider />
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button StartIcon={FiTrash} variant="icon" color="destructive" aria-label={t("delete")} />
|
<Button
|
||||||
|
StartIcon={FiTrash}
|
||||||
|
variant="icon"
|
||||||
|
color="destructive"
|
||||||
|
aria-label={t("delete")}
|
||||||
|
disabled={schedule?.isLastSchedule}
|
||||||
|
tooltip={t("requires_at_least_one_schedule")}
|
||||||
|
/>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isLoading={deleteMutation.isLoading}
|
||||||
|
|||||||
@@ -93,7 +93,13 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
key={schedule.id}
|
key={schedule.id}
|
||||||
schedule={schedule}
|
schedule={schedule}
|
||||||
updateDefault={updateMutation.mutate}
|
updateDefault={updateMutation.mutate}
|
||||||
deleteFunction={deleteMutation.mutate}
|
deleteFunction={() => {
|
||||||
|
if (schedules.length === 1) {
|
||||||
|
showToast(t("requires_at_least_one_schedule"), "error");
|
||||||
|
} else {
|
||||||
|
deleteMutation.mutate;
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { RRule } from "rrule";
|
import { RRule } from "rrule";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -36,6 +36,7 @@ import {
|
|||||||
formatToLocalizedTimezone,
|
formatToLocalizedTimezone,
|
||||||
} from "@calcom/lib/date-fns";
|
} from "@calcom/lib/date-fns";
|
||||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||||
|
import { getBookingWithResponses } from "@calcom/lib/getBooking";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
@@ -51,7 +52,6 @@ import { Button, EmailInput, HeadSeo, Badge } from "@calcom/ui";
|
|||||||
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
|
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import { getBookingWithResponses } from "@lib/getBooking";
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import CancelBooking from "@components/booking/CancelBooking";
|
import CancelBooking from "@components/booking/CancelBooking";
|
||||||
@@ -59,114 +59,6 @@ import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
|||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
function redirectToExternalUrl(url: string) {
|
|
||||||
window.parent.location.href = url;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirects to external URL with query params from current URL.
|
|
||||||
* Query Params and Hash Fragment if present in external URL are kept intact.
|
|
||||||
*/
|
|
||||||
function RedirectionToast({ url }: { url: string }) {
|
|
||||||
const [timeRemaining, setTimeRemaining] = useState(10);
|
|
||||||
const [isToastVisible, setIsToastVisible] = useState(true);
|
|
||||||
|
|
||||||
const { t } = useLocale();
|
|
||||||
const timerRef = useRef<number | null>(null);
|
|
||||||
const router = useRouter();
|
|
||||||
const { cancel: isCancellationMode } = querySchema.parse(router.query);
|
|
||||||
const urlWithSuccessParamsRef = useRef<string | null>();
|
|
||||||
if (isCancellationMode && timerRef.current) {
|
|
||||||
setIsToastVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isToastVisible && timerRef.current) {
|
|
||||||
window.clearInterval(timerRef.current);
|
|
||||||
}
|
|
||||||
}, [isToastVisible]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const parsedExternalUrl = new URL(url);
|
|
||||||
|
|
||||||
const parsedSuccessUrl = new URL(document.URL);
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
/* @ts-ignore */ //https://stackoverflow.com/questions/49218765/typescript-and-iterator-type-iterableiteratort-is-not-an-array-type
|
|
||||||
for (const [name, value] of parsedExternalUrl.searchParams.entries()) {
|
|
||||||
parsedSuccessUrl.searchParams.set(name, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
const urlWithSuccessParams =
|
|
||||||
parsedExternalUrl.origin +
|
|
||||||
parsedExternalUrl.pathname +
|
|
||||||
"?" +
|
|
||||||
parsedSuccessUrl.searchParams.toString() +
|
|
||||||
parsedExternalUrl.hash;
|
|
||||||
urlWithSuccessParamsRef.current = urlWithSuccessParams;
|
|
||||||
|
|
||||||
timerRef.current = window.setInterval(() => {
|
|
||||||
if (timeRemaining > 0) {
|
|
||||||
setTimeRemaining((timeRemaining) => {
|
|
||||||
return timeRemaining - 1;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
redirectToExternalUrl(urlWithSuccessParams);
|
|
||||||
window.clearInterval(timerRef.current as number);
|
|
||||||
}
|
|
||||||
}, 1000);
|
|
||||||
return () => {
|
|
||||||
window.clearInterval(timerRef.current as number);
|
|
||||||
};
|
|
||||||
}, [timeRemaining, url]);
|
|
||||||
|
|
||||||
if (!isToastVisible) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className="relative z-[60] pb-2 sm:pb-5">
|
|
||||||
<div className="mx-auto w-full sm:max-w-7xl sm:px-2 lg:px-8">
|
|
||||||
<div className="border border-green-600 bg-green-500 p-2 sm:p-3">
|
|
||||||
<div className="flex flex-wrap items-center justify-between">
|
|
||||||
<div className="flex w-0 flex-1 items-center">
|
|
||||||
<p className="truncate font-medium text-white sm:mx-3">
|
|
||||||
<span className="md:hidden">Redirecting to {url} ...</span>
|
|
||||||
<span className="hidden md:inline">
|
|
||||||
{t("you_are_being_redirected", { url, seconds: timeRemaining })}
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="order-3 mt-2 w-full flex-shrink-0 sm:order-2 sm:mt-0 sm:w-auto">
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
if (urlWithSuccessParamsRef.current) {
|
|
||||||
redirectToExternalUrl(urlWithSuccessParamsRef.current);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="flex w-full items-center justify-center rounded-sm border border-transparent bg-white px-4 py-2 text-sm font-medium text-green-600 hover:bg-green-50">
|
|
||||||
{t("continue")}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="order-2 flex-shrink-0 sm:order-3 sm:ml-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setIsToastVisible(false);
|
|
||||||
}}
|
|
||||||
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
|
|
||||||
<FiX className="h-6 w-6 text-white" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
type SuccessProps = inferSSRProps<typeof getServerSideProps>;
|
type SuccessProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
|
|
||||||
const stringToBoolean = z
|
const stringToBoolean = z
|
||||||
@@ -176,13 +68,14 @@ const stringToBoolean = z
|
|||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
uid: z.string(),
|
uid: z.string(),
|
||||||
allRemainingBookings: stringToBoolean,
|
email: z.string().optional(),
|
||||||
|
eventTypeSlug: z.string().optional(),
|
||||||
cancel: stringToBoolean,
|
cancel: stringToBoolean,
|
||||||
|
allRemainingBookings: stringToBoolean,
|
||||||
changes: stringToBoolean,
|
changes: stringToBoolean,
|
||||||
reschedule: stringToBoolean,
|
reschedule: stringToBoolean,
|
||||||
isSuccessBookingPage: z.string().optional(),
|
isSuccessBookingPage: stringToBoolean,
|
||||||
formerTime: z.string().optional(),
|
formerTime: z.string().optional(),
|
||||||
email: z.string().optional(),
|
|
||||||
seatReferenceUid: z.string().optional(),
|
seatReferenceUid: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -266,7 +159,11 @@ export default function Success(props: SuccessProps) {
|
|||||||
const giphyImage = giphyAppData?.thankYouPage;
|
const giphyImage = giphyAppData?.thankYouPage;
|
||||||
|
|
||||||
const eventName = getEventName(eventNameObject, true);
|
const eventName = getEventName(eventNameObject, true);
|
||||||
const needsConfirmation = eventType.requiresConfirmation && reschedule != true;
|
// Confirmation can be needed in two cases as of now
|
||||||
|
// - Event Type has require confirmation option enabled always
|
||||||
|
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
|
||||||
|
// - It's a paid event and payment is pending.
|
||||||
|
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING;
|
||||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||||
|
|
||||||
const isCancelled =
|
const isCancelled =
|
||||||
@@ -400,9 +297,6 @@ export default function Success(props: SuccessProps) {
|
|||||||
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
|
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
|
||||||
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
||||||
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
||||||
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
|
|
||||||
<RedirectionToast url={eventType.successRedirectUrl} />
|
|
||||||
) : null}{" "}
|
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "text-center" : "",
|
shouldAlignCentrally ? "text-center" : "",
|
||||||
@@ -485,7 +379,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<div className="font-medium">{t("what")}</div>
|
<div className="font-medium">{t("what")}</div>
|
||||||
<div className="col-span-2 mb-6 last:mb-0">{eventName}</div>
|
<div className="col-span-2 mb-6 last:mb-0">{props.bookingInfo.title}</div>
|
||||||
<div className="font-medium">{t("when")}</div>
|
<div className="font-medium">{t("when")}</div>
|
||||||
<div className="col-span-2 mb-6 last:mb-0">
|
<div className="col-span-2 mb-6 last:mb-0">
|
||||||
{reschedule && !!formerTime && (
|
{reschedule && !!formerTime && (
|
||||||
@@ -527,7 +421,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{bookingInfo?.attendees.map((attendee) => (
|
{bookingInfo?.attendees.map((attendee) => (
|
||||||
<div key={attendee.name} className="mb-3 last:mb-0">
|
<div key={attendee.name + attendee.email} className="mb-3 last:mb-0">
|
||||||
{attendee.name && <p>{attendee.name}</p>}
|
{attendee.name && <p>{attendee.name}</p>}
|
||||||
<p data-testid={`attendee-${attendee.email}`} className="text-bookinglight">
|
<p data-testid={`attendee-${attendee.email}`} className="text-bookinglight">
|
||||||
{attendee.email}
|
{attendee.email}
|
||||||
@@ -565,7 +459,8 @@ export default function Success(props: SuccessProps) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-bookingdark dark:border-darkgray-200 mt-8 text-left dark:text-gray-300">
|
||||||
{Object.entries(bookingInfo.responses).map(([name, response]) => {
|
{Object.entries(bookingInfo.responses).map(([name, response]) => {
|
||||||
const field = eventType.bookingFields.find((field) => field.name === name);
|
const field = eventType.bookingFields.find((field) => field.name === name);
|
||||||
// We show location in the "where" section
|
// We show location in the "where" section
|
||||||
@@ -580,15 +475,10 @@ export default function Success(props: SuccessProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-9 font-medium">{label}</div>
|
<div className="mt-4 font-medium">{label}</div>
|
||||||
<div className="col-span-2 mb-2 mt-9">
|
<p className="break-words" data-testid="field-response" data-fob-field={field.name}>
|
||||||
<p
|
{response.toString()}
|
||||||
className="break-words"
|
</p>
|
||||||
data-testid="field-response"
|
|
||||||
data-fob-field={field.name}>
|
|
||||||
{response.toString()}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -981,13 +871,6 @@ const getEventTypesFromDB = async (id: number) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const schema = z.object({
|
|
||||||
uid: z.string(),
|
|
||||||
email: z.string().optional(),
|
|
||||||
eventTypeSlug: z.string().optional(),
|
|
||||||
cancel: stringToBoolean,
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleSeatsEventTypeOnBooking = (
|
const handleSeatsEventTypeOnBooking = (
|
||||||
eventType: {
|
eventType: {
|
||||||
seatsPerTimeSlot?: number | null;
|
seatsPerTimeSlot?: number | null;
|
||||||
@@ -1017,9 +900,9 @@ const handleSeatsEventTypeOnBooking = (
|
|||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const parsedQuery = schema.safeParse(context.query);
|
const parsedQuery = querySchema.safeParse(context.query);
|
||||||
if (!parsedQuery.success) return { notFound: true };
|
if (!parsedQuery.success) return { notFound: true };
|
||||||
const { uid, email, eventTypeSlug, cancel } = parsedQuery.data;
|
const { uid, email, eventTypeSlug, cancel, isSuccessBookingPage } = parsedQuery.data;
|
||||||
|
|
||||||
const bookingInfoRaw = await prisma.booking.findFirst({
|
const bookingInfoRaw = await prisma.booking.findFirst({
|
||||||
where: {
|
where: {
|
||||||
@@ -1087,8 +970,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw);
|
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||||
|
|
||||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||||
// probably fixable with json.stringify -> json.parse
|
// probably fixable with json.stringify -> json.parse
|
||||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||||
@@ -1117,7 +999,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) {
|
if (eventType.successRedirectUrl && isSuccessBookingPage && !cancel) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
destination: eventType.successRedirectUrl,
|
destination: eventType.successRedirectUrl,
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import type { LocationObject } from "@calcom/core/location";
|
|||||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getWorkingHours } from "@calcom/lib/availability";
|
import { getWorkingHours } from "@calcom/lib/availability";
|
||||||
|
import type { GetBookingType } from "@calcom/lib/getBooking";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
|
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import type { GetBookingType } from "@lib/getBooking";
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
@@ -119,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
hideBranding: u.hideBranding,
|
hideBranding: u.hideBranding,
|
||||||
timeZone: u.timeZone,
|
timeZone: u.timeZone,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(hashedLink.eventType.description),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [user] = users;
|
const [user] = users;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
@@ -93,6 +94,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
brandColor: u.brandColor,
|
brandColor: u.brandColor,
|
||||||
darkBrandColor: u.darkBrandColor,
|
darkBrandColor: u.darkBrandColor,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
};
|
};
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
@@ -120,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
profile,
|
profile,
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
booking: null,
|
booking: null,
|
||||||
|
currentSlotBooking: null,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
isDynamicGroupBooking: false,
|
isDynamicGroupBooking: false,
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ import type { EventLocationType } from "@calcom/core/location";
|
|||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import getEventTypeById from "@calcom/lib/getEventTypeById";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
@@ -65,6 +63,7 @@ export type FormValues = {
|
|||||||
hostPhoneNumber?: string;
|
hostPhoneNumber?: string;
|
||||||
displayLocationPublicly?: boolean;
|
displayLocationPublicly?: boolean;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
hostDefault?: string;
|
||||||
}[];
|
}[];
|
||||||
customInputs: CustomInputParsed[];
|
customInputs: CustomInputParsed[];
|
||||||
schedule: number | null;
|
schedule: number | null;
|
||||||
@@ -365,9 +364,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
|
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
|
const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
|
||||||
if (isLoading || !data) return null;
|
|
||||||
return <EventTypePage {...data} />;
|
return <EventTypePage {...(data as EventTypeSetupProps)} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
@@ -393,26 +392,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
|
||||||
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
|
return {
|
||||||
|
props: {
|
||||||
return {
|
type: typeParam,
|
||||||
props: {
|
trpcState: ssr.dehydrate(),
|
||||||
// session,
|
},
|
||||||
type: typeParam,
|
};
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
initialData: {
|
|
||||||
eventType: res.eventType,
|
|
||||||
locationOptions: res.locationOptions,
|
|
||||||
team: res.team,
|
|
||||||
teamMembers: res.teamMembers,
|
|
||||||
currentUserMembership: res.currentUserMembership,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
} catch (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EventTypePageWrapper;
|
export default EventTypePageWrapper;
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
|||||||
</div>
|
</div>
|
||||||
<EventTypeDescription
|
<EventTypeDescription
|
||||||
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||||
eventType={type}
|
eventType={{ ...type, descriptionAsSafeHTML: type.safeDescription }}
|
||||||
shortenDescription
|
shortenDescription
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||||
|
import {
|
||||||
|
AverageEventDurationChart,
|
||||||
|
BookingKPICards,
|
||||||
|
BookingStatusLineChart,
|
||||||
|
LeastBookedTeamMembersTable,
|
||||||
|
MostBookedTeamMembersTable,
|
||||||
|
PopularEventsTable,
|
||||||
|
} from "@calcom/features/insights/components";
|
||||||
|
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
|
||||||
|
import { Filters } from "@calcom/features/insights/filters";
|
||||||
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
|
import { UpgradeTip } from "@calcom/features/tips";
|
||||||
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc";
|
||||||
|
import { Button, ButtonGroup } from "@calcom/ui";
|
||||||
|
import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
const Heading = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-w-52 hidden md:block">
|
||||||
|
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 truncate text-xl font-semibold tracking-wide text-black xl:max-w-full">
|
||||||
|
{t("analytics_for_organisation")}
|
||||||
|
</h3>
|
||||||
|
<p className="hidden text-sm text-gray-500 md:block">{t("subtitle_analytics")}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function InsightsPage() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { data: user } = trpc.viewer.me.useQuery();
|
||||||
|
const features = [
|
||||||
|
{
|
||||||
|
icon: <FiUsers className="h-5 w-5" />,
|
||||||
|
title: t("view_bookings_across"),
|
||||||
|
description: t("view_bookings_across_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FiRefreshCcw className="h-5 w-5" />,
|
||||||
|
title: t("identify_booking_trends"),
|
||||||
|
description: t("identify_booking_trends_description"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
icon: <FiUserPlus className="h-5 w-5" />,
|
||||||
|
title: t("spot_popular_event_types"),
|
||||||
|
description: t("spot_popular_event_types_description"),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<Shell>
|
||||||
|
<UpgradeTip
|
||||||
|
title={t("make_informed_decisions")}
|
||||||
|
description={t("make_informed_decisions_description")}
|
||||||
|
features={features}
|
||||||
|
background="/tips/insights.jpg"
|
||||||
|
buttons={
|
||||||
|
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||||
|
<ButtonGroup>
|
||||||
|
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||||
|
{t("create_team")}
|
||||||
|
</Button>
|
||||||
|
<Button color="secondary" href="https://go.cal.com/insights" target="_blank">
|
||||||
|
{t("learn_more")}
|
||||||
|
</Button>
|
||||||
|
</ButtonGroup>
|
||||||
|
</div>
|
||||||
|
}>
|
||||||
|
{!user ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<FiltersProvider>
|
||||||
|
<div className="mb-8 mt-0 ml-auto flex w-full flex-wrap justify-between md:-mt-8">
|
||||||
|
<Heading />
|
||||||
|
<Filters />
|
||||||
|
</div>
|
||||||
|
<div className="mb-4 space-y-6">
|
||||||
|
<BookingKPICards />
|
||||||
|
|
||||||
|
<BookingStatusLineChart />
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||||
|
<PopularEventsTable />
|
||||||
|
|
||||||
|
<AverageEventDurationChart />
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||||
|
<MostBookedTeamMembersTable />
|
||||||
|
<LeastBookedTeamMembersTable />
|
||||||
|
</div>
|
||||||
|
<small className="block text-center text-gray-600">
|
||||||
|
{t("looking_for_more_insights")}
|
||||||
|
<a
|
||||||
|
className="text-blue-500 hover:underline"
|
||||||
|
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
|
||||||
|
{" "}
|
||||||
|
{t("contact_support")}
|
||||||
|
</a>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
</FiltersProvider>
|
||||||
|
)}
|
||||||
|
</UpgradeTip>
|
||||||
|
</Shell>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If feature flag is disabled, return not found on getServerSideProps
|
||||||
|
export const getServerSideProps = async () => {
|
||||||
|
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||||
|
const flags = await getFeatureFlagMap(prisma);
|
||||||
|
|
||||||
|
if (flags.insights === false) {
|
||||||
|
return {
|
||||||
|
notFound: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
|
||||||
|
|
||||||
|
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||||
|
|
||||||
|
const FlagsPage = () => <FlagListingView />;
|
||||||
|
|
||||||
|
FlagsPage.getLayout = getLayout;
|
||||||
|
|
||||||
|
export default FlagsPage;
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { IdentityProvider } from "@prisma/client";
|
import { IdentityProvider } from "@prisma/client";
|
||||||
import crypto from "crypto";
|
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import type { BaseSyntheticEvent } from "react";
|
import type { BaseSyntheticEvent } from "react";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
@@ -332,11 +331,6 @@ const ProfileForm = ({
|
|||||||
bio: z.string(),
|
bio: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const emailMd5 = crypto
|
|
||||||
.createHash("md5")
|
|
||||||
.update(defaultValues.email || "example@example.com")
|
|
||||||
.digest("hex");
|
|
||||||
|
|
||||||
const formMethods = useForm<FormValues>({
|
const formMethods = useForm<FormValues>({
|
||||||
defaultValues,
|
defaultValues,
|
||||||
resolver: zodResolver(profileFormSchema),
|
resolver: zodResolver(profileFormSchema),
|
||||||
@@ -356,7 +350,7 @@ const ProfileForm = ({
|
|||||||
name="avatar"
|
name="avatar"
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<>
|
<>
|
||||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
<Avatar alt="" imageSrc={value} gravatarFallbackMd5="fallback" size="lg" />
|
||||||
<div className="ltr:ml-4 rtl:mr-4">
|
<div className="ltr:ml-4 rtl:mr-4">
|
||||||
<ImageUploader
|
<ImageUploader
|
||||||
target="avatar"
|
target="avatar"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useState } from "react";
|
|||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText, Alert } from "@calcom/ui";
|
||||||
|
|
||||||
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
||||||
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
||||||
@@ -32,11 +32,14 @@ const TwoFactorAuthView = () => {
|
|||||||
|
|
||||||
if (isLoading) return <SkeletonLoader />;
|
if (isLoading) return <SkeletonLoader />;
|
||||||
|
|
||||||
|
const isCalProvider = user?.identityProvider === "CAL";
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title={t("2fa")} description={t("2fa_description")} />
|
<Meta title={t("2fa")} description={t("2fa_description")} />
|
||||||
|
{!isCalProvider && <Alert severity="neutral" message={t("2fa_disabled")} />}
|
||||||
<div className="mt-6 flex items-start space-x-4">
|
<div className="mt-6 flex items-start space-x-4">
|
||||||
<Switch
|
<Switch
|
||||||
|
disabled={!isCalProvider}
|
||||||
checked={user?.twoFactorEnabled}
|
checked={user?.twoFactorEnabled}
|
||||||
onCheckedChange={() =>
|
onCheckedChange={() =>
|
||||||
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
|
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const OnboardTeamMembersPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
|
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
|
||||||
<WizardLayout currentStep={1} maxSteps={2}>
|
<WizardLayout currentStep={2} maxSteps={2}>
|
||||||
{page}
|
{page}
|
||||||
</WizardLayout>
|
</WizardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Head from "next/head";
|
|||||||
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import { getLayout } from "@components/layouts/WizardLayout";
|
import WizardLayout from "@components/layouts/WizardLayout";
|
||||||
|
|
||||||
const CreateNewTeamPage = () => {
|
const CreateNewTeamPage = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -17,7 +17,14 @@ const CreateNewTeamPage = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const LayoutWrapper = (page: React.ReactElement) => {
|
||||||
|
return (
|
||||||
|
<WizardLayout currentStep={1} maxSteps={2}>
|
||||||
|
{page}
|
||||||
|
</WizardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
CreateNewTeamPage.getLayout = getLayout;
|
CreateNewTeamPage.getLayout = LayoutWrapper;
|
||||||
|
|
||||||
export default CreateNewTeamPage;
|
export default CreateNewTeamPage;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { CAL_URL } from "@calcom/lib/constants";
|
|||||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -113,7 +113,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: team.safeBio }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -187,11 +187,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
...user,
|
...user,
|
||||||
avatar: CAL_URL + "/" + user.username + "/avatar.png",
|
avatar: CAL_URL + "/" + user.username + "/avatar.png",
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const safeBio = markdownToSafeHTML(team.bio) || "";
|
||||||
|
|
||||||
|
const members = team.members.map((member) => {
|
||||||
|
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
team,
|
team: { ...team, safeBio, members },
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
|
|
||||||
import type { LocationObject } from "@calcom/core/location";
|
import type { LocationObject } from "@calcom/core/location";
|
||||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getWorkingHours } from "@calcom/lib/availability";
|
import { getWorkingHours } from "@calcom/lib/availability";
|
||||||
|
import getBooking from "@calcom/lib/getBooking";
|
||||||
|
import type { GetBookingType } from "@calcom/lib/getBooking";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import type { GetBookingType } from "@lib/getBooking";
|
|
||||||
import getBooking from "@lib/getBooking";
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
@@ -64,7 +64,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
avatar: true,
|
|
||||||
username: true,
|
username: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
@@ -171,13 +170,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
hideBranding,
|
hideBranding,
|
||||||
timeZone,
|
timeZone,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
});
|
});
|
||||||
|
|
||||||
eventTypeObject.availability = [];
|
eventTypeObject.availability = [];
|
||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
if (rescheduleUid) {
|
if (rescheduleUid) {
|
||||||
booking = await getBooking(prisma, rescheduleUid, getBookingFieldsWithSystemFields(eventTypeObject));
|
booking = await getBooking(prisma, rescheduleUid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
|
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
|
||||||
|
|||||||
@@ -5,12 +5,13 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
|||||||
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import type { GetBookingType } from "@calcom/lib/getBooking";
|
||||||
|
import getBooking from "@calcom/lib/getBooking";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import type { GetBookingType } from "@lib/getBooking";
|
|
||||||
import getBooking from "@lib/getBooking";
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import BookingPage from "@components/booking/pages/BookingPage";
|
import BookingPage from "@components/booking/pages/BookingPage";
|
||||||
@@ -121,13 +122,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
image: u.avatar,
|
image: u.avatar,
|
||||||
slug: u.username,
|
slug: u.username,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
};
|
};
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
let booking: GetBookingType | null = null;
|
let booking: GetBookingType | null = null;
|
||||||
const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
|
const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
|
||||||
if (rescheduleUid || bookingUid) {
|
if (rescheduleUid || bookingUid) {
|
||||||
booking = await getBooking(prisma, rescheduleUid || bookingUid || "", eventTypeObject.bookingFields);
|
booking = await getBooking(prisma, rescheduleUid || bookingUid || "");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checking if number of recurring event ocurrances is valid against event type configuration
|
// Checking if number of recurring event ocurrances is valid against event type configuration
|
||||||
@@ -152,6 +154,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
booking,
|
booking,
|
||||||
|
currentSlotBooking: null,
|
||||||
isDynamicGroupBooking: false,
|
isDynamicGroupBooking: false,
|
||||||
hasHashedBookingLink: false,
|
hasHashedBookingLink: false,
|
||||||
hashedLink: null,
|
hashedLink: null,
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ test.describe("Booking with Seats", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(oldBooking?.attendees).toBeFalsy();
|
expect(oldBooking?.status).toBe(BookingStatus.CANCELLED);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Should cancel with seats and have no attendees and cancelled", async ({
|
test("Should cancel with seats and have no attendees and cancelled", async ({
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ test.describe("Event Types tests", () => {
|
|||||||
await page.locator("#location-select").click();
|
await page.locator("#location-select").click();
|
||||||
await page.locator("text=In Person (Organizer Address)").click();
|
await page.locator("text=In Person (Organizer Address)").click();
|
||||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||||
await page.waitForTimeout(250);
|
await page.waitForTimeout(1000);
|
||||||
await page.locator('input[name="locationAddress"]').fill(inputText);
|
await page.locator('input[name="locationAddress"]').fill(inputText);
|
||||||
await page.locator("[data-testid=display-location]").check();
|
await page.locator("[data-testid=display-location]").check();
|
||||||
await page.locator("[data-testid=update-location]").click();
|
await page.locator("[data-testid=update-location]").click();
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const createEmbedsFixture = (page: Page) => {
|
|||||||
}
|
}
|
||||||
api("on", {
|
api("on", {
|
||||||
action: "*",
|
action: "*",
|
||||||
callback: (e: any) => {
|
callback: (e) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
|
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { Page } from "@playwright/test";
|
|||||||
import { expect } from "@playwright/test";
|
import { expect } from "@playwright/test";
|
||||||
import type { IncomingMessage, ServerResponse } from "http";
|
import type { IncomingMessage, ServerResponse } from "http";
|
||||||
import { createServer } from "http";
|
import { createServer } from "http";
|
||||||
import noop from "lodash/noop";
|
import { noop } from "lodash";
|
||||||
|
|
||||||
import { test } from "./fixtures";
|
import { test } from "./fixtures";
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,11 @@ test.describe("Onboarding", () => {
|
|||||||
await page.locator("input[name=username]").fill("new user onboarding");
|
await page.locator("input[name=username]").fill("new user onboarding");
|
||||||
await page.locator("input[name=name]").fill("new user 2");
|
await page.locator("input[name=name]").fill("new user 2");
|
||||||
await page.locator("input[role=combobox]").click();
|
await page.locator("input[role=combobox]").click();
|
||||||
await page.locator("text=Europe/London GMT +0:00").click();
|
await page
|
||||||
|
.locator("*")
|
||||||
|
.filter({ hasText: /^Europe\/London/ })
|
||||||
|
.first()
|
||||||
|
.click();
|
||||||
|
|
||||||
await page.locator("button[type=submit]").click();
|
await page.locator("button[type=submit]").click();
|
||||||
|
|
||||||
|
|||||||
@@ -97,12 +97,19 @@ test.describe("Reschedule Tests", async () => {
|
|||||||
|
|
||||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
// NOTE: remove if old booking should not be deleted
|
const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking.uid } });
|
||||||
expect(await (await booking.self()).status).toBe(BookingStatus.CANCELLED);
|
const rescheduledBooking = await prisma.booking.findFirstOrThrow({ where: { uid: booking.uid } });
|
||||||
|
|
||||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } });
|
|
||||||
expect(newBooking).not.toBeNull();
|
expect(newBooking).not.toBeNull();
|
||||||
await prisma.booking.delete({ where: { id: newBooking?.id } });
|
expect(rescheduledBooking.status).toBe(BookingStatus.CANCELLED);
|
||||||
|
|
||||||
|
await prisma.booking.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: [newBooking.id, rescheduledBooking.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => {
|
test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => {
|
||||||
@@ -148,7 +155,6 @@ test.describe("Reschedule Tests", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
await expect(page).toHaveURL(/.*payment/);
|
await expect(page).toHaveURL(/.*payment/);
|
||||||
await payment.delete();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
|
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
|
||||||
@@ -172,8 +178,6 @@ test.describe("Reschedule Tests", async () => {
|
|||||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||||
|
|
||||||
await expect(page).toHaveURL(/.*booking/);
|
await expect(page).toHaveURL(/.*booking/);
|
||||||
|
|
||||||
await payment.delete();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Opt in event should be PENDING when rescheduled by USER", async ({ page, users, bookings }) => {
|
test("Opt in event should be PENDING when rescheduled by USER", async ({ page, users, bookings }) => {
|
||||||
|
|||||||
@@ -4,4 +4,6 @@ Disallow: /api
|
|||||||
Disallow: /static/locales
|
Disallow: /static/locales
|
||||||
|
|
||||||
# Specifically allow access to OG Image api, otherwise eg Twitter won't render these images.
|
# Specifically allow access to OG Image api, otherwise eg Twitter won't render these images.
|
||||||
Allow: /api/social/og/image
|
Allow: /api/social/og/image
|
||||||
|
# Specifically allow access to api/app-store to allow search engines to fetch assets
|
||||||
|
Allow: /api/app-store/
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 56 KiB |
@@ -111,7 +111,7 @@
|
|||||||
"hidden_team_owner_message": "You need a pro account to use teams, you are hidden until you upgrade.",
|
"hidden_team_owner_message": "You need a pro account to use teams, you are hidden until you upgrade.",
|
||||||
"link_expires": "p.s. It expires in {{expiresIn}} hours.",
|
"link_expires": "p.s. It expires in {{expiresIn}} hours.",
|
||||||
"upgrade_to_per_seat": "Upgrade to Per-Seat",
|
"upgrade_to_per_seat": "Upgrade to Per-Seat",
|
||||||
"seat_options_doesnt_support_confirmation":"Seats option doesn't support confirmation requirement",
|
"seat_options_doesnt_support_confirmation": "Seats option doesn't support confirmation requirement",
|
||||||
"team_upgrade_seats_details": "Of the {{memberCount}} members in your team, {{unpaidCount}} seat(s) are unpaid. At ${{seatPrice}}/month per seat the estimated total cost of your membership is ${{totalCost}}/month.",
|
"team_upgrade_seats_details": "Of the {{memberCount}} members in your team, {{unpaidCount}} seat(s) are unpaid. At ${{seatPrice}}/month per seat the estimated total cost of your membership is ${{totalCost}}/month.",
|
||||||
"team_upgrade_banner_description": "Thank you for trialing our new team plan. We noticed your team \"{{teamName}}\" needs to be upgraded.",
|
"team_upgrade_banner_description": "Thank you for trialing our new team plan. We noticed your team \"{{teamName}}\" needs to be upgraded.",
|
||||||
"team_upgrade_banner_action": "Upgrade here",
|
"team_upgrade_banner_action": "Upgrade here",
|
||||||
@@ -404,6 +404,7 @@
|
|||||||
"add_time_availability": "Add new time slot",
|
"add_time_availability": "Add new time slot",
|
||||||
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
|
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
|
||||||
"2fa": "Two-Factor Authentication",
|
"2fa": "Two-Factor Authentication",
|
||||||
|
"2fa_disabled": "Two-Factor authentication can only be enabled for email and password authentication",
|
||||||
"enable_2fa": "Enable two-factor authentication",
|
"enable_2fa": "Enable two-factor authentication",
|
||||||
"disable_2fa": "Disable two-factor authentication",
|
"disable_2fa": "Disable two-factor authentication",
|
||||||
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
|
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
|
||||||
@@ -433,7 +434,7 @@
|
|||||||
"password_hint_min": "Minimum 7 characters long",
|
"password_hint_min": "Minimum 7 characters long",
|
||||||
"password_hint_admin_min": "Minimum 15 characters long",
|
"password_hint_admin_min": "Minimum 15 characters long",
|
||||||
"password_hint_num": "Contain at least 1 number",
|
"password_hint_num": "Contain at least 1 number",
|
||||||
"max_limit_allowed_hint":"Must be {{limit}} or fewer characters long",
|
"max_limit_allowed_hint": "Must be {{limit}} or fewer characters long",
|
||||||
"invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters",
|
"invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters",
|
||||||
"incorrect_password": "Password is incorrect.",
|
"incorrect_password": "Password is incorrect.",
|
||||||
"incorrect_username_password": "Username or password is incorrect.",
|
"incorrect_username_password": "Username or password is incorrect.",
|
||||||
@@ -863,7 +864,7 @@
|
|||||||
"add_new_calendar": "Add a new calendar",
|
"add_new_calendar": "Add a new calendar",
|
||||||
"set_calendar": "Set where to add new events to when you're booked.",
|
"set_calendar": "Set where to add new events to when you're booked.",
|
||||||
"delete_schedule": "Delete schedule",
|
"delete_schedule": "Delete schedule",
|
||||||
"delete_schedule_description":"Deleting a schedule will remove it from all event types. This action cannot be undone.",
|
"delete_schedule_description": "Deleting a schedule will remove it from all event types. This action cannot be undone.",
|
||||||
"schedule_created_successfully": "{{scheduleName}} schedule created successfully",
|
"schedule_created_successfully": "{{scheduleName}} schedule created successfully",
|
||||||
"availability_updated_successfully": "{{scheduleName}} schedule updated successfully",
|
"availability_updated_successfully": "{{scheduleName}} schedule updated successfully",
|
||||||
"schedule_deleted_successfully": "Schedule deleted successfully",
|
"schedule_deleted_successfully": "Schedule deleted successfully",
|
||||||
@@ -1218,6 +1219,7 @@
|
|||||||
"impersonation": "Impersonation",
|
"impersonation": "Impersonation",
|
||||||
"impersonation_description": "Settings to manage user impersonation",
|
"impersonation_description": "Settings to manage user impersonation",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
|
"user": "User",
|
||||||
"profile_description": "Manage settings for your {{appName}} profile",
|
"profile_description": "Manage settings for your {{appName}} profile",
|
||||||
"users_description": "Here you can find a list of all users",
|
"users_description": "Here you can find a list of all users",
|
||||||
"users_listing": "User listing",
|
"users_listing": "User listing",
|
||||||
@@ -1499,7 +1501,7 @@
|
|||||||
"date_overrides_update_btn": "Update Override",
|
"date_overrides_update_btn": "Update Override",
|
||||||
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
||||||
"set_as_default": "Set as default",
|
"set_as_default": "Set as default",
|
||||||
"hide_eventtype_details": "Hide EventType Details",
|
"hide_eventtype_details": "Hide event type details",
|
||||||
"show_navigation": "Show navigation",
|
"show_navigation": "Show navigation",
|
||||||
"hide_navigation": "Hide navigation",
|
"hide_navigation": "Hide navigation",
|
||||||
"verification_code_sent": "Verification code sent",
|
"verification_code_sent": "Verification code sent",
|
||||||
@@ -1513,7 +1515,7 @@
|
|||||||
"create_your_first_team_webhook_description": "Create your first webhook for this team event type",
|
"create_your_first_team_webhook_description": "Create your first webhook for this team event type",
|
||||||
"create_webhook_team_event_type": "Create a webhook for this team event type",
|
"create_webhook_team_event_type": "Create a webhook for this team event type",
|
||||||
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
|
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
|
||||||
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters",
|
"invalid_admin_password": "You are admin but you do not have a password length of at least 15 characters or no 2FA yet",
|
||||||
"change_password_admin": "Change Password to gain admin access",
|
"change_password_admin": "Change Password to gain admin access",
|
||||||
"username_already_taken": "Username is already taken",
|
"username_already_taken": "Username is already taken",
|
||||||
"assignment": "Assignment",
|
"assignment": "Assignment",
|
||||||
@@ -1608,6 +1610,7 @@
|
|||||||
"default_app_link_title": "Set a default app link",
|
"default_app_link_title": "Set a default app link",
|
||||||
"default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.",
|
"default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.",
|
||||||
"change_default_conferencing_app": "Set as default",
|
"change_default_conferencing_app": "Set as default",
|
||||||
|
"organizer_default_conferencing_app": "Organizer's default app",
|
||||||
"under_maintenance": "Down for maintenance",
|
"under_maintenance": "Down for maintenance",
|
||||||
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
|
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
|
||||||
"event_type_seats": "{{numberOfSeats}} seats",
|
"event_type_seats": "{{numberOfSeats}} seats",
|
||||||
@@ -1631,22 +1634,30 @@
|
|||||||
"a_routing_form": "A Routing Form",
|
"a_routing_form": "A Routing Form",
|
||||||
"form_description_placeholder": "Form Description",
|
"form_description_placeholder": "Form Description",
|
||||||
"keep_me_connected_with_form": "Keep me connected with the form",
|
"keep_me_connected_with_form": "Keep me connected with the form",
|
||||||
"fields_in_form_duplicated":"Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.",
|
"fields_in_form_duplicated": "Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.",
|
||||||
"form_deleted": "Form deleted",
|
"form_deleted": "Form deleted",
|
||||||
"delete_form": "Are you sure you want to delete this form?",
|
"delete_form": "Are you sure you want to delete this form?",
|
||||||
"delete_form_action": "Yes, delete Form",
|
"delete_form_action": "Yes, delete Form",
|
||||||
"delete_form_confirmation":"Anyone who you've shared the link with will no longer be able to access it.",
|
"delete_form_confirmation": "Anyone who you've shared the link with will no longer be able to access it.",
|
||||||
"delete_form_confirmation_2":"All associated responses will be deleted.",
|
"delete_form_confirmation_2": "All associated responses will be deleted.",
|
||||||
"typeform_redirect_url_copied": "Typeform Redirect URL copied! You can go and set the URL in Typeform form.",
|
"typeform_redirect_url_copied": "Typeform Redirect URL copied! You can go and set the URL in Typeform form.",
|
||||||
"modifications_in_fields_warning": "Modifications in fields and routes of following forms will be reflected in this form.",
|
"modifications_in_fields_warning": "Modifications in fields and routes of following forms will be reflected in this form.",
|
||||||
"connected_forms": "Connected Forms",
|
"connected_forms": "Connected Forms",
|
||||||
"form_modifications_warning": "Following forms would be affected when you modify fields or routes here.",
|
"form_modifications_warning": "Following forms would be affected when you modify fields or routes here.",
|
||||||
"responses_collection_waiting_description": "Wait for some time for responses to be collected. You can go and submit the form yourself as well.",
|
"responses_collection_waiting_description": "Wait for some time for responses to be collected. You can go and submit the form yourself as well.",
|
||||||
"this_is_what_your_users_would_see":"This is what your users would see",
|
"this_is_what_your_users_would_see": "This is what your users would see",
|
||||||
"identifies_name_field": "Identifies field by this name.",
|
"identifies_name_field": "Identifies field by this name.",
|
||||||
"add_1_option_per_line": "Add 1 option per line",
|
"add_1_option_per_line": "Add 1 option per line",
|
||||||
"select_a_router": "Select a router",
|
"select_a_router": "Select a router",
|
||||||
"add_a_new_route": "Add a new Route",
|
"add_a_new_route": "Add a new Route",
|
||||||
|
"make_informed_decisions": "Make informed decisions with Insights",
|
||||||
|
"make_informed_decisions_description": "Our Insights dashboard surfaces all activity across your team and shows you trends that enable better team scheduling and decision making.",
|
||||||
|
"view_bookings_across": "View bookings across all members",
|
||||||
|
"view_bookings_across_description": "See who’s receiving the most bookings and ensure the best distribution across your team",
|
||||||
|
"identify_booking_trends": "Identify booking trends",
|
||||||
|
"identify_booking_trends_description": "See what times of the week and what times during the day are popular for your bookers",
|
||||||
|
"spot_popular_event_types": "Spot popular event types",
|
||||||
|
"spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings",
|
||||||
"no_responses_yet": "No responses yet",
|
"no_responses_yet": "No responses yet",
|
||||||
"this_will_be_the_placeholder": "This will be the placeholder",
|
"this_will_be_the_placeholder": "This will be the placeholder",
|
||||||
"this_meeting_has_not_started_yet": "This meeting has not started yet",
|
"this_meeting_has_not_started_yet": "This meeting has not started yet",
|
||||||
@@ -1657,8 +1668,33 @@
|
|||||||
"verification_code": "Verification code",
|
"verification_code": "Verification code",
|
||||||
"can_you_try_again": "Can you try again with a different time?",
|
"can_you_try_again": "Can you try again with a different time?",
|
||||||
"verify": "Verify",
|
"verify": "Verify",
|
||||||
"invalid_event_name_variables":"There is an invalid variable in your event name",
|
"invalid_event_name_variables": "There is an invalid variable in your event name",
|
||||||
"select_all": "Select All",
|
"select_all": "Select All",
|
||||||
"default_conferencing_bulk_title": "Bulk update existing event types",
|
"default_conferencing_bulk_title": "Bulk update existing event types",
|
||||||
"default_conferencing_bulk_description": "Update the locations for the selected event types"
|
"requires_at_least_one_schedule": "You are required to have at least one schedule",
|
||||||
|
"default_conferencing_bulk_description": "Update the locations for the selected event types",
|
||||||
|
"looking_for_more_insights": "Looking for more Insights?",
|
||||||
|
"add_filter": "Add filter",
|
||||||
|
"select_user": "Select User",
|
||||||
|
"select_event_type": "Select Event Type",
|
||||||
|
"select_date_range": "Select Date Range",
|
||||||
|
"popular_events": "Popular Events",
|
||||||
|
"no_event_types_found": "No event types found",
|
||||||
|
"average_event_duration": "Average Event Duration",
|
||||||
|
"most_booked_members": "Most Booked Members",
|
||||||
|
"least_booked_members": "Least Booked Members",
|
||||||
|
"events_created": "Events Created",
|
||||||
|
"events_completed": "Events Completed",
|
||||||
|
"events_cancelled": "Events Cancelled",
|
||||||
|
"events_rescheduled": "Events Rescheduled",
|
||||||
|
"from_last_period": "from last period",
|
||||||
|
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
|
||||||
|
"analytics_for_organisation": "Insights",
|
||||||
|
"subtitle_analytics": "Learn more about your team's activity",
|
||||||
|
"redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.",
|
||||||
|
"event_trends": "Event Trends",
|
||||||
|
"clear_filters": "Clear Filters",
|
||||||
|
"insights": "Insights",
|
||||||
|
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
|
||||||
|
"email_invite_team": "{{email}} has been invited"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1213,6 +1213,7 @@
|
|||||||
"impersonation": "Suplantación",
|
"impersonation": "Suplantación",
|
||||||
"impersonation_description": "Configuración para administrar la suplantación del usuario",
|
"impersonation_description": "Configuración para administrar la suplantación del usuario",
|
||||||
"users": "Usuarios",
|
"users": "Usuarios",
|
||||||
|
"user": "Usuario",
|
||||||
"profile_description": "Administra los ajustes para tu perfil de {{appName}}",
|
"profile_description": "Administra los ajustes para tu perfil de {{appName}}",
|
||||||
"users_description": "Aquí puede encontrar una lista de todos los usuarios",
|
"users_description": "Aquí puede encontrar una lista de todos los usuarios",
|
||||||
"users_listing": "Lista de usuarios",
|
"users_listing": "Lista de usuarios",
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 46 KiB |
|
After Width: | Height: | Size: 155 KiB |
|
After Width: | Height: | Size: 204 KiB |
|
After Width: | Height: | Size: 173 KiB |
@@ -29,7 +29,6 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
|
|||||||
ctx: {
|
ctx: {
|
||||||
prisma,
|
prisma,
|
||||||
session: null,
|
session: null,
|
||||||
user: null,
|
|
||||||
locale,
|
locale,
|
||||||
i18n: _i18n,
|
i18n: _i18n,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -148,8 +148,6 @@ select:focus {
|
|||||||
--tw-ring-color: var(--brand-color);
|
--tw-ring-color: var(--brand-color);
|
||||||
border-color: var(--brand-color);
|
border-color: var(--brand-color);
|
||||||
}
|
}
|
||||||
/* https://stackoverflow.com/a/69308190 */
|
|
||||||
input[type='text']:focus { box-shadow: none; }
|
|
||||||
|
|
||||||
|
|
||||||
@layer components {
|
@layer components {
|
||||||
@@ -497,3 +495,5 @@ hr {
|
|||||||
::-webkit-search-cancel-button {
|
::-webkit-search-cancel-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,5 @@ const base = require("@calcom/config/tailwind-preset");
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...base,
|
...base,
|
||||||
content: [...base.content],
|
content: [...base.content, "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -70,6 +70,7 @@
|
|||||||
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"type-check": "turbo run type-check",
|
"type-check": "turbo run type-check",
|
||||||
|
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
||||||
"web": "yarn workspace @calcom/web"
|
"web": "yarn workspace @calcom/web"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -86,8 +87,9 @@
|
|||||||
"jest-watch-select-projects": "^2.0.0",
|
"jest-watch-select-projects": "^2.0.0",
|
||||||
"jest-watch-typeahead": "^2.0.0",
|
"jest-watch-typeahead": "^2.0.0",
|
||||||
"lint-staged": "^12.5.0",
|
"lint-staged": "^12.5.0",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.6",
|
||||||
"ts-jest": "^28.0.8",
|
"ts-jest": "^28.0.8",
|
||||||
|
"tsc-absolute": "^1.0.0",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
|
import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
|
||||||
|
|
||||||
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
|
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
|
||||||
import execSync from "./utils/execSync";
|
import execSync from "./utils/execSync";
|
||||||
@@ -127,6 +127,8 @@ export const BaseAppFork = {
|
|||||||
.replace(/_DESCRIPTION_/g, description)
|
.replace(/_DESCRIPTION_/g, description)
|
||||||
.replace(/_APP_DIR_/g, slug)
|
.replace(/_APP_DIR_/g, slug)
|
||||||
);
|
);
|
||||||
|
// New monorepo package has been added, so we need to run yarn again
|
||||||
|
await execSync("yarn");
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) {
|
delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) {
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export const EventTypeAddonMap = {
|
|||||||
fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")),
|
fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")),
|
||||||
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
|
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
|
||||||
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
|
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
|
||||||
|
"google-tag-manager": dynamic(() => import("./google-tag-manager/components/EventTypeAppCardInterface")),
|
||||||
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
|
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
|
||||||
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
|
qr_code: dynamic(() => import("./qr_code/components/EventTypeAppCardInterface")),
|
||||||
rainbow: dynamic(() => import("./rainbow/components/EventTypeAppCardInterface")),
|
rainbow: dynamic(() => import("./rainbow/components/EventTypeAppCardInterface")),
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { appKeysSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
|
|||||||
import { appKeysSchema as fathom_zod_ts } from "./fathom/zod";
|
import { appKeysSchema as fathom_zod_ts } from "./fathom/zod";
|
||||||
import { appKeysSchema as ga4_zod_ts } from "./ga4/zod";
|
import { appKeysSchema as ga4_zod_ts } from "./ga4/zod";
|
||||||
import { appKeysSchema as giphy_zod_ts } from "./giphy/zod";
|
import { appKeysSchema as giphy_zod_ts } from "./giphy/zod";
|
||||||
|
import { appKeysSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
|
||||||
import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
import { appKeysSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
||||||
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
|
import { appKeysSchema as hubspot_zod_ts } from "./hubspot/zod";
|
||||||
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
||||||
@@ -30,6 +31,7 @@ export const appKeysSchemas = {
|
|||||||
fathom: fathom_zod_ts,
|
fathom: fathom_zod_ts,
|
||||||
ga4: ga4_zod_ts,
|
ga4: ga4_zod_ts,
|
||||||
giphy: giphy_zod_ts,
|
giphy: giphy_zod_ts,
|
||||||
|
"google-tag-manager": google_tag_manager_zod_ts,
|
||||||
googlecalendar: googlecalendar_zod_ts,
|
googlecalendar: googlecalendar_zod_ts,
|
||||||
hubspot: hubspot_zod_ts,
|
hubspot: hubspot_zod_ts,
|
||||||
larkcalendar: larkcalendar_zod_ts,
|
larkcalendar: larkcalendar_zod_ts,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import facetime_config_json from "./facetime/config.json";
|
|||||||
import fathom_config_json from "./fathom/config.json";
|
import fathom_config_json from "./fathom/config.json";
|
||||||
import ga4_config_json from "./ga4/config.json";
|
import ga4_config_json from "./ga4/config.json";
|
||||||
import { metadata as giphy__metadata_ts } from "./giphy/_metadata";
|
import { metadata as giphy__metadata_ts } from "./giphy/_metadata";
|
||||||
|
import google_tag_manager_config_json from "./google-tag-manager/config.json";
|
||||||
import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata";
|
import { metadata as googlecalendar__metadata_ts } from "./googlecalendar/_metadata";
|
||||||
import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
|
import { metadata as googlevideo__metadata_ts } from "./googlevideo/_metadata";
|
||||||
import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
|
import { metadata as hubspot__metadata_ts } from "./hubspot/_metadata";
|
||||||
@@ -39,6 +40,7 @@ import sendgrid_config_json from "./sendgrid/config.json";
|
|||||||
import signal_config_json from "./signal/config.json";
|
import signal_config_json from "./signal/config.json";
|
||||||
import sirius_video_config_json from "./sirius_video/config.json";
|
import sirius_video_config_json from "./sirius_video/config.json";
|
||||||
import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata";
|
import { metadata as stripepayment__metadata_ts } from "./stripepayment/_metadata";
|
||||||
|
import sylapsvideo_config_json from "./sylapsvideo/config.json";
|
||||||
import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata";
|
import { metadata as tandemvideo__metadata_ts } from "./tandemvideo/_metadata";
|
||||||
import telegram_config_json from "./telegram/config.json";
|
import telegram_config_json from "./telegram/config.json";
|
||||||
import basic_config_json from "./templates/basic/config.json";
|
import basic_config_json from "./templates/basic/config.json";
|
||||||
@@ -75,6 +77,7 @@ export const appStoreMetadata = {
|
|||||||
fathom: fathom_config_json,
|
fathom: fathom_config_json,
|
||||||
ga4: ga4_config_json,
|
ga4: ga4_config_json,
|
||||||
giphy: giphy__metadata_ts,
|
giphy: giphy__metadata_ts,
|
||||||
|
"google-tag-manager": google_tag_manager_config_json,
|
||||||
googlecalendar: googlecalendar__metadata_ts,
|
googlecalendar: googlecalendar__metadata_ts,
|
||||||
googlevideo: googlevideo__metadata_ts,
|
googlevideo: googlevideo__metadata_ts,
|
||||||
hubspot: hubspot__metadata_ts,
|
hubspot: hubspot__metadata_ts,
|
||||||
@@ -97,6 +100,7 @@ export const appStoreMetadata = {
|
|||||||
signal: signal_config_json,
|
signal: signal_config_json,
|
||||||
sirius_video: sirius_video_config_json,
|
sirius_video: sirius_video_config_json,
|
||||||
stripepayment: stripepayment__metadata_ts,
|
stripepayment: stripepayment__metadata_ts,
|
||||||
|
sylapsvideo: sylapsvideo_config_json,
|
||||||
tandemvideo: tandemvideo__metadata_ts,
|
tandemvideo: tandemvideo__metadata_ts,
|
||||||
telegram: telegram_config_json,
|
telegram: telegram_config_json,
|
||||||
basic: basic_config_json,
|
basic: basic_config_json,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { appDataSchema as dailyvideo_zod_ts } from "./dailyvideo/zod";
|
|||||||
import { appDataSchema as fathom_zod_ts } from "./fathom/zod";
|
import { appDataSchema as fathom_zod_ts } from "./fathom/zod";
|
||||||
import { appDataSchema as ga4_zod_ts } from "./ga4/zod";
|
import { appDataSchema as ga4_zod_ts } from "./ga4/zod";
|
||||||
import { appDataSchema as giphy_zod_ts } from "./giphy/zod";
|
import { appDataSchema as giphy_zod_ts } from "./giphy/zod";
|
||||||
|
import { appDataSchema as google_tag_manager_zod_ts } from "./google-tag-manager/zod";
|
||||||
import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
import { appDataSchema as googlecalendar_zod_ts } from "./googlecalendar/zod";
|
||||||
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
|
import { appDataSchema as hubspot_zod_ts } from "./hubspot/zod";
|
||||||
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
import { appDataSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
||||||
@@ -30,6 +31,7 @@ export const appDataSchemas = {
|
|||||||
fathom: fathom_zod_ts,
|
fathom: fathom_zod_ts,
|
||||||
ga4: ga4_zod_ts,
|
ga4: ga4_zod_ts,
|
||||||
giphy: giphy_zod_ts,
|
giphy: giphy_zod_ts,
|
||||||
|
"google-tag-manager": google_tag_manager_zod_ts,
|
||||||
googlecalendar: googlecalendar_zod_ts,
|
googlecalendar: googlecalendar_zod_ts,
|
||||||
hubspot: hubspot_zod_ts,
|
hubspot: hubspot_zod_ts,
|
||||||
larkcalendar: larkcalendar_zod_ts,
|
larkcalendar: larkcalendar_zod_ts,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ export const apiHandlers = {
|
|||||||
fathom: import("./fathom/api"),
|
fathom: import("./fathom/api"),
|
||||||
ga4: import("./ga4/api"),
|
ga4: import("./ga4/api"),
|
||||||
giphy: import("./giphy/api"),
|
giphy: import("./giphy/api"),
|
||||||
|
"google-tag-manager": import("./google-tag-manager/api"),
|
||||||
googlecalendar: import("./googlecalendar/api"),
|
googlecalendar: import("./googlecalendar/api"),
|
||||||
googlevideo: import("./googlevideo/api"),
|
googlevideo: import("./googlevideo/api"),
|
||||||
hubspot: import("./hubspot/api"),
|
hubspot: import("./hubspot/api"),
|
||||||
@@ -39,6 +40,7 @@ export const apiHandlers = {
|
|||||||
signal: import("./signal/api"),
|
signal: import("./signal/api"),
|
||||||
sirius_video: import("./sirius_video/api"),
|
sirius_video: import("./sirius_video/api"),
|
||||||
stripepayment: import("./stripepayment/api"),
|
stripepayment: import("./stripepayment/api"),
|
||||||
|
sylapsvideo: import("./sylapsvideo/api"),
|
||||||
tandemvideo: import("./tandemvideo/api"),
|
tandemvideo: import("./tandemvideo/api"),
|
||||||
telegram: import("./telegram/api"),
|
telegram: import("./telegram/api"),
|
||||||
basic: import("./templates/basic/api"),
|
basic: import("./templates/basic/api"),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ items:
|
|||||||
{DESCRIPTION}
|
{DESCRIPTION}
|
||||||
|
|
||||||
### Cron's most valuable features
|
### Cron's most valuable features
|
||||||
|
|
||||||
<!-- From "FAQs: What are Cron’s most valuable features?"
|
<!-- From "FAQs: What are Cron’s most valuable features?"
|
||||||
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
|
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
|
||||||
|
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
|
||||||
|
|
||||||
|
import appConfig from "../config.json";
|
||||||
|
|
||||||
|
const handler: AppDeclarativeHandler = {
|
||||||
|
appType: appConfig.type,
|
||||||
|
variant: appConfig.variant,
|
||||||
|
slug: appConfig.slug,
|
||||||
|
supportsMultipleInstalls: false,
|
||||||
|
handlerType: "add",
|
||||||
|
createCredential: ({ appType, user, slug }) =>
|
||||||
|
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default handler;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as add } from "./add";
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useAppContextWithSchema } from "@calcom/app-store/EventTypeAppContext";
|
||||||
|
import AppCard from "@calcom/app-store/_components/AppCard";
|
||||||
|
import type { EventTypeAppCardComponent } from "@calcom/app-store/types";
|
||||||
|
import { TextField } from "@calcom/ui";
|
||||||
|
|
||||||
|
import type { appDataSchema } from "../zod";
|
||||||
|
|
||||||
|
const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({ app }) {
|
||||||
|
const [getAppData, setAppData] = useAppContextWithSchema<typeof appDataSchema>();
|
||||||
|
const trackingId = getAppData("trackingId");
|
||||||
|
const [enabled, setEnabled] = useState(getAppData("enabled"));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppCard
|
||||||
|
setAppData={setAppData}
|
||||||
|
app={app}
|
||||||
|
switchOnClick={(e) => {
|
||||||
|
if (!e) {
|
||||||
|
setEnabled(false);
|
||||||
|
} else {
|
||||||
|
setEnabled(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
switchChecked={enabled}>
|
||||||
|
<TextField
|
||||||
|
name="Tracking ID"
|
||||||
|
value={trackingId}
|
||||||
|
onChange={(e) => {
|
||||||
|
setAppData("trackingId", e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</AppCard>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EventTypeAppCard;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{
|
||||||
|
"name": "Google Tag Manager",
|
||||||
|
"slug": "google-tag-manager",
|
||||||
|
"type": "google-tag-manager_analytics",
|
||||||
|
"imageSrc": "icon.svg",
|
||||||
|
"logo": "icon.svg",
|
||||||
|
"url": "https://tagmanager.google.com",
|
||||||
|
"variant": "analytics",
|
||||||
|
"categories": [
|
||||||
|
"analytics"
|
||||||
|
],
|
||||||
|
"publisher": "Black Lemon",
|
||||||
|
"email": "support@blacklemon.dk",
|
||||||
|
"description": "App to install Google Tag Manager",
|
||||||
|
"extendsFeature": "EventType",
|
||||||
|
"appData": {
|
||||||
|
"tag": {
|
||||||
|
"scripts": [
|
||||||
|
{
|
||||||
|
"content":"(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);})(window,document,'script','dataLayer','GTM-{TRACKING_ID}');"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"isTemplate": false,
|
||||||
|
"__createdUsingCli": true,
|
||||||
|
"__template": "booking-pages-tag"
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export * as api from "./api";
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/package.json",
|
||||||
|
"private": true,
|
||||||
|
"name": "@calcom/google-tag-manager",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"main": "./index.ts",
|
||||||
|
"dependencies": {
|
||||||
|
"@calcom/lib": "*"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@calcom/types": "*"
|
||||||
|
},
|
||||||
|
"description": "App to install Google Tag Manager"
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 2.2 MiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.87 122.89"><defs><style>.cls-1{fill:#8ab4f8;}.cls-2{fill:#4285f4;}.cls-3{fill:#246fdb;}</style></defs><title>google-tag-manager</title><polygon class="cls-1" points="72.13 118.22 50.8 96.86 96.63 50.56 118.35 72.28 72.13 118.22 72.13 118.22"/><path class="cls-2" d="M72.29,26.23,50.56,4.5,4.5,50.56a15.35,15.35,0,0,0,0,21.71h0l46.06,46.06,21.31-21.4L37.07,61.45,72.29,26.23Z"/><path class="cls-1" d="M118.35,50.56,72.29,4.5A15.36,15.36,0,0,0,50.56,26.22L96.65,72.28a15.36,15.36,0,0,0,21.72-21.72Z"/><path class="cls-3" d="M76.48,107.66a15.23,15.23,0,1,0-15.22,15.23,15.22,15.22,0,0,0,15.22-15.23Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 695 B |
@@ -0,0 +1,11 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { eventTypeAppCardZod } from "@calcom/app-store/eventTypeAppCardZod";
|
||||||
|
|
||||||
|
export const appDataSchema = eventTypeAppCardZod.merge(
|
||||||
|
z.object({
|
||||||
|
trackingId: z.string(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
export const appKeysSchema = z.object({});
|
||||||
@@ -133,6 +133,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
calendarId: selectedCalendar,
|
calendarId: selectedCalendar,
|
||||||
requestBody: payload,
|
requestBody: payload,
|
||||||
conferenceDataVersion: 1,
|
conferenceDataVersion: 1,
|
||||||
|
sendUpdates: "none",
|
||||||
},
|
},
|
||||||
function (error, event) {
|
function (error, event) {
|
||||||
if (error || !event?.data) {
|
if (error || !event?.data) {
|
||||||
@@ -164,6 +165,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
type: "google_calendar",
|
type: "google_calendar",
|
||||||
password: "",
|
password: "",
|
||||||
url: "",
|
url: "",
|
||||||
|
iCalUID: event.data.iCalUID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -200,6 +202,9 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
id: String(event.organizer.id),
|
id: String(event.organizer.id),
|
||||||
organizer: true,
|
organizer: true,
|
||||||
responseStatus: "accepted",
|
responseStatus: "accepted",
|
||||||
|
email: event.destinationCalendar?.externalId
|
||||||
|
? event.destinationCalendar.externalId
|
||||||
|
: event.organizer.email,
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
...eventAttendees,
|
...eventAttendees,
|
||||||
@@ -234,14 +239,13 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
calendarId: selectedCalendar,
|
calendarId: selectedCalendar,
|
||||||
eventId: uid,
|
eventId: uid,
|
||||||
sendNotifications: true,
|
sendNotifications: true,
|
||||||
sendUpdates: "all",
|
sendUpdates: "none",
|
||||||
requestBody: payload,
|
requestBody: payload,
|
||||||
conferenceDataVersion: 1,
|
conferenceDataVersion: 1,
|
||||||
},
|
},
|
||||||
function (err, evt) {
|
function (err, evt) {
|
||||||
if (err) {
|
if (err) {
|
||||||
console.error("There was an error contacting google calendar service: ", err);
|
console.error("There was an error contacting google calendar service: ", err);
|
||||||
|
|
||||||
return reject(err);
|
return reject(err);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -268,6 +272,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
type: "google_calendar",
|
type: "google_calendar",
|
||||||
password: "",
|
password: "",
|
||||||
url: "",
|
url: "",
|
||||||
|
iCalUID: evt.data.iCalUID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return resolve(evt?.data);
|
return resolve(evt?.data);
|
||||||
@@ -293,7 +298,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
calendarId: calendarId ? calendarId : defaultCalendarId,
|
calendarId: calendarId ? calendarId : defaultCalendarId,
|
||||||
eventId: uid,
|
eventId: uid,
|
||||||
sendNotifications: false,
|
sendNotifications: false,
|
||||||
sendUpdates: "all",
|
sendUpdates: "none",
|
||||||
},
|
},
|
||||||
function (err: GoogleCalError | null, event) {
|
function (err: GoogleCalError | null, event) {
|
||||||
if (err) {
|
if (err) {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import * as plausible from "./plausible";
|
|||||||
import * as salesforce from "./salesforce";
|
import * as salesforce from "./salesforce";
|
||||||
import * as sendgrid from "./sendgrid";
|
import * as sendgrid from "./sendgrid";
|
||||||
import * as stripepayment from "./stripepayment";
|
import * as stripepayment from "./stripepayment";
|
||||||
|
import * as sylapsvideo from "./sylapsvideo";
|
||||||
import * as tandemvideo from "./tandemvideo";
|
import * as tandemvideo from "./tandemvideo";
|
||||||
import * as vital from "./vital";
|
import * as vital from "./vital";
|
||||||
import * as wipemycalother from "./wipemycalother";
|
import * as wipemycalother from "./wipemycalother";
|
||||||
@@ -38,6 +39,7 @@ const appStore = {
|
|||||||
hubspot,
|
hubspot,
|
||||||
huddle01video,
|
huddle01video,
|
||||||
jitsivideo,
|
jitsivideo,
|
||||||
|
sylapsvideo,
|
||||||
larkcalendar,
|
larkcalendar,
|
||||||
office365calendar,
|
office365calendar,
|
||||||
office365video,
|
office365video,
|
||||||
|
|||||||
@@ -12,13 +12,20 @@ export type DefaultEventLocationType = {
|
|||||||
type: DefaultEventLocationTypeEnum;
|
type: DefaultEventLocationTypeEnum;
|
||||||
label: string;
|
label: string;
|
||||||
messageForOrganizer: string;
|
messageForOrganizer: string;
|
||||||
category: "in person" | "other" | "phone";
|
category: "in person" | "conferencing" | "other" | "phone";
|
||||||
|
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
urlRegExp?: string;
|
urlRegExp?: string;
|
||||||
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
|
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
|
||||||
variable: "locationType" | "locationAddress" | "address" | "locationLink" | "locationPhoneNumber" | "phone";
|
variable:
|
||||||
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone";
|
| "locationType"
|
||||||
|
| "locationAddress"
|
||||||
|
| "address"
|
||||||
|
| "locationLink"
|
||||||
|
| "locationPhoneNumber"
|
||||||
|
| "phone"
|
||||||
|
| "hostDefault";
|
||||||
|
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone";
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
organizerInputType: "phone" | "text" | null;
|
organizerInputType: "phone" | "text" | null;
|
||||||
@@ -60,6 +67,7 @@ export enum DefaultEventLocationTypeEnum {
|
|||||||
*/
|
*/
|
||||||
UserPhone = "userPhone",
|
UserPhone = "userPhone",
|
||||||
Link = "link",
|
Link = "link",
|
||||||
|
Conferencing = "conferencing",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultLocations: DefaultEventLocationType[] = [
|
export const defaultLocations: DefaultEventLocationType[] = [
|
||||||
@@ -88,6 +96,17 @@ export const defaultLocations: DefaultEventLocationType[] = [
|
|||||||
iconUrl: "/map-pin.svg",
|
iconUrl: "/map-pin.svg",
|
||||||
category: "in person",
|
category: "in person",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
default: true,
|
||||||
|
type: DefaultEventLocationTypeEnum.Conferencing,
|
||||||
|
iconUrl: "/link.svg",
|
||||||
|
organizerInputType: null,
|
||||||
|
label: "organizer_default_conferencing_app",
|
||||||
|
variable: "hostDefault",
|
||||||
|
defaultValueVariable: "hostDefault",
|
||||||
|
category: "conferencing",
|
||||||
|
messageForOrganizer: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
default: true,
|
default: true,
|
||||||
type: DefaultEventLocationTypeEnum.Link,
|
type: DefaultEventLocationTypeEnum.Link,
|
||||||
@@ -130,7 +149,9 @@ export const defaultLocations: DefaultEventLocationType[] = [
|
|||||||
export type LocationObject = {
|
export type LocationObject = {
|
||||||
type: string;
|
type: string;
|
||||||
displayLocationPublicly?: boolean;
|
displayLocationPublicly?: boolean;
|
||||||
} & Partial<Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone", string>>;
|
} & Partial<
|
||||||
|
Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone", string>
|
||||||
|
>;
|
||||||
|
|
||||||
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
|
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
|
||||||
export type BookingLocationValue = string;
|
export type BookingLocationValue = string;
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
body: JSON.stringify(this.translateEvent(event)),
|
body: JSON.stringify(this.translateEvent(event)),
|
||||||
});
|
});
|
||||||
|
|
||||||
return handleErrorsJson(response);
|
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
|
||||||
|
|
||||||
|
return { ...responseJson, iCalUID: responseJson.iCalUId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.error(error);
|
this.log.error(error);
|
||||||
|
|
||||||
@@ -88,7 +90,9 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
body: JSON.stringify(this.translateEvent(event)),
|
body: JSON.stringify(this.translateEvent(event)),
|
||||||
});
|
});
|
||||||
|
|
||||||
return handleErrorsRaw(response);
|
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
|
||||||
|
|
||||||
|
return { ...responseJson, iCalUID: responseJson.iCalUId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.error(error);
|
this.log.error(error);
|
||||||
|
|
||||||
@@ -219,7 +223,7 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
};
|
};
|
||||||
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
||||||
|
|
||||||
const refreshAccessToken = async (refreshToken: string) => {
|
const refreshAccessToken = async (o365AuthCredentials: O365AuthCredentials) => {
|
||||||
const { client_id, client_secret } = await getOfficeAppKeys();
|
const { client_id, client_secret } = await getOfficeAppKeys();
|
||||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -227,12 +231,22 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
body: new URLSearchParams({
|
body: new URLSearchParams({
|
||||||
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||||
client_id,
|
client_id,
|
||||||
refresh_token: refreshToken,
|
refresh_token: o365AuthCredentials.refresh_token,
|
||||||
grant_type: "refresh_token",
|
grant_type: "refresh_token",
|
||||||
client_secret,
|
client_secret,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const o365AuthCredentials = refreshTokenResponseSchema.parse(await handleErrorsJson(response));
|
const responseJson = await handleErrorsJson(response);
|
||||||
|
const tokenResponse = refreshTokenResponseSchema.safeParse(responseJson);
|
||||||
|
o365AuthCredentials = { ...o365AuthCredentials, ...(tokenResponse.success && tokenResponse.data) };
|
||||||
|
if (!tokenResponse.success) {
|
||||||
|
console.error(
|
||||||
|
"Outlook error grabbing new tokens ~ zodError:",
|
||||||
|
tokenResponse.error,
|
||||||
|
"MS response:",
|
||||||
|
responseJson
|
||||||
|
);
|
||||||
|
}
|
||||||
await prisma.credential.update({
|
await prisma.credential.update({
|
||||||
where: {
|
where: {
|
||||||
id: credential.id,
|
id: credential.id,
|
||||||
@@ -246,9 +260,10 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
getToken: () =>
|
getToken: () =>
|
||||||
|
refreshTokenResponseSchema.safeParse(o365AuthCredentials).success &&
|
||||||
!isExpired(o365AuthCredentials.expires_in)
|
!isExpired(o365AuthCredentials.expires_in)
|
||||||
? Promise.resolve(o365AuthCredentials.access_token)
|
? Promise.resolve(o365AuthCredentials.access_token)
|
||||||
: refreshAccessToken(o365AuthCredentials.refresh_token),
|
: refreshAccessToken(o365AuthCredentials),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||||
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
|
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
|
||||||
|
|||||||