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 | ||
|
|
efb9bf7116 | ||
|
|
49ee5d300b | ||
|
|
1512551903 | ||
|
|
1b459eb212 | ||
|
|
e4eb21bf8d | ||
|
|
79f7a16f62 | ||
|
|
7fc884912e | ||
|
|
20b3efbcb6 | ||
|
|
427b19238d | ||
|
|
696122a701 | ||
|
|
7d9c21cf9a | ||
|
|
eb93524f49 | ||
|
|
07fd3ad29f | ||
|
|
389633394b | ||
|
|
d27817ad51 | ||
|
|
7af23305eb | ||
|
|
db947591ad | ||
|
|
2ef4a05750 | ||
|
|
f1ac45ee19 | ||
|
|
03c9fc7952 | ||
|
|
de2a56ebcb | ||
|
|
0bae688da1 | ||
|
|
7967a65ade | ||
|
|
1cd96d4c25 | ||
|
|
19bc329377 | ||
|
|
0c29f2a135 | ||
|
|
a145de56ee | ||
|
|
d2352da54b | ||
|
|
5d7685f2c0 | ||
|
|
ee8b2e67f0 | ||
|
|
a0c9123ee7 | ||
|
|
131611e114 | ||
|
|
b3407e19b5 | ||
|
|
0d4b0847d4 | ||
|
|
5d8575abf4 | ||
|
|
631bf4d9fe | ||
|
|
8fd1d76227 | ||
|
|
22b189cb19 | ||
|
|
27c29c65e2 | ||
|
|
f0c7d7ff29 | ||
|
|
679965782e | ||
|
|
650ecc7a85 | ||
|
|
8d27dcdea3 | ||
|
|
6128be8860 | ||
|
|
13577a5dbe | ||
|
|
9ba13a3f3c | ||
|
|
9ac562cfba | ||
|
|
e1411b3fb2 | ||
|
|
0f89415ec3 | ||
|
|
c6ff3dca4a | ||
|
|
e001c0b88f | ||
|
|
5cbd334a42 | ||
|
|
470ef737b5 | ||
|
|
f3e8bed1cf | ||
|
|
750362c0cf | ||
|
|
3811dd564d | ||
|
|
99fe67b879 | ||
|
|
2685e8154f | ||
|
|
4308ea8d52 | ||
|
|
358aa8be2b | ||
|
|
b20c0df4e8 | ||
|
|
09af14b3f4 | ||
|
|
77df4117d0 | ||
|
|
84f7924215 | ||
|
|
c8554e6878 | ||
|
|
5993e0842c | ||
|
|
7179b09d7b | ||
|
|
325dc9805f | ||
|
|
0f6a678b74 |
@@ -20,12 +20,17 @@
|
||||
# - APP STORE **********************************************************************************************
|
||||
# ⚠️ ⚠️ ⚠️ THESE WILL BE MIGRATED TO THE DATABASE TO PREVENT AWS's 4KB ENV QUOTA ⚠️ ⚠️ ⚠️
|
||||
# - DAILY.CO VIDEO
|
||||
# Enables Cal Video. to get your key
|
||||
# 1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
||||
# 2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
||||
# @see https://github.com/calcom/cal.com#obtaining-daily-api-credentials
|
||||
|
||||
DAILY_API_KEY=
|
||||
DAILY_SCALE_PLAN=''
|
||||
|
||||
# - GOOGLE CALENDAR/MEET/LOGIN
|
||||
# Needed to enable Google Calendar integration and Login with Google
|
||||
# @see https://github.com/calendso/calendso#obtaining-the-google-api-credentials
|
||||
# @see https://github.com/calcom/cal.com#obtaining-the-google-api-credentials
|
||||
GOOGLE_API_CREDENTIALS=
|
||||
|
||||
# To enable Login with Google you need to:
|
||||
|
||||
+6
-3
@@ -22,10 +22,13 @@ CALCOM_LICENSE_KEY=
|
||||
|
||||
# - DATABASE ************************************************************************************************
|
||||
DATABASE_URL="postgresql://postgres:@localhost:5450/calendso"
|
||||
# Opt-in to a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you will be able to scale your DB independently of your application.
|
||||
|
||||
# Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy
|
||||
# Cold boots will be faster and you'll be able to scale your DB independently of your app.
|
||||
# @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy
|
||||
PRISMA_GENERATE_DATAPROXY=false
|
||||
# PRISMA_GENERATE_DATAPROXY=true
|
||||
PRISMA_GENERATE_DATAPROXY=
|
||||
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# - SHARED **************************************************************************************************
|
||||
|
||||
@@ -12,13 +12,13 @@ Fixes # (issue)
|
||||
|
||||
## 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)
|
||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- Bug fix (non-breaking change which fixes an issue)
|
||||
- Chore (refactoring code, technical debt, workflow improvements)
|
||||
- New feature (non-breaking change which adds functionality)
|
||||
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- This change requires a documentation update
|
||||
|
||||
## 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: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Show info
|
||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||
|
||||
- run: yarn type-check
|
||||
- name: Configure TSC problem matcher
|
||||
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
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# "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
|
||||
schedule:
|
||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
||||
- cron: "0,15,30,45 * * * *"
|
||||
# Runs “Every month at 1st (see https://crontab.guru)
|
||||
- cron: "0 0 1 * *"
|
||||
jobs:
|
||||
cron-downgradeUsers:
|
||||
env:
|
||||
|
||||
@@ -26,6 +26,6 @@ jobs:
|
||||
localization_branch_name: main
|
||||
create_pull_request: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
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);
|
||||
})();
|
||||
@@ -88,6 +88,7 @@ That's where Cal.com comes in. Self-hosted or hosted by us. White-label by desig
|
||||
- [React](https://reactjs.org/?ref=cal.com)
|
||||
- [Tailwind](https://tailwindcss.com/?ref=cal.com)
|
||||
- [Prisma](https://prisma.io/?ref=cal.com)
|
||||
- [Daily.co](https://go.cal.com/daily)
|
||||
|
||||
## Stay Up-to-Date
|
||||
|
||||
@@ -349,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">
|
||||
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
</a>
|
||||
|
||||
<!-- CONTRIBUTORS -->
|
||||
|
||||
@@ -432,7 +432,7 @@ following
|
||||
|
||||
### Obtaining Daily API Credentials
|
||||
|
||||
1. Open [Daily](https://www.daily.co/) and sign into your account.
|
||||
1. Visit our [Daily.co Partnership Form](https://go.cal.com/daily) and enter your information
|
||||
2. From within your dashboard, go to the [developers](https://dashboard.daily.co/developers) tab.
|
||||
3. Copy your API key.
|
||||
4. Now paste the API key to your .env file into the `DAILY_API_KEY` field in your .env file.
|
||||
@@ -455,12 +455,12 @@ following
|
||||
|
||||
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".
|
||||
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
|
||||
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.
|
||||
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.
|
||||
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:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
||||
"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.",
|
||||
"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,147 +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) }),
|
||||
option: (defaultStyles, state) => ({
|
||||
...defaultStyles,
|
||||
backgroundColor: state.isSelected
|
||||
? state.isFocused
|
||||
? "var(--brand-color)"
|
||||
: "var(--brand-color)"
|
||||
: state.isFocused
|
||||
? "var(--brand-color-dark-mode)"
|
||||
: "var(--brand-text-color)",
|
||||
}),
|
||||
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;
|
||||
+152
-162
@@ -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 type { NextRouter } from "next/router";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
TextField,
|
||||
ColorPicker,
|
||||
} 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";
|
||||
|
||||
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
|
||||
const height = getDimension(previewState.inline.height);
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
@@ -187,12 +187,12 @@ function MyComponent() {
|
||||
}) => {
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
const cal = await getCalApi();
|
||||
Cal("floatingButton", ${floatingButtonArg});
|
||||
cal("floatingButton", ${floatingButtonArg});
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
@@ -201,7 +201,7 @@ function MyComponent() {
|
||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
@@ -313,7 +313,7 @@ const embeds: {
|
||||
}[] = [
|
||||
{
|
||||
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",
|
||||
illustration: (
|
||||
<svg
|
||||
@@ -327,10 +327,10 @@ const embeds: {
|
||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||
fill="white"
|
||||
/>
|
||||
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||
<rect x="24.5" y="51" width="139" height="163" rx="1.5" fill="#F8F8F8" />
|
||||
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="2" fill="#E1E1E1" />
|
||||
<rect x="48" y="86.5" width="48" height="4" rx="1" fill="#E1E1E1" />
|
||||
<rect opacity="0.8" x="48" y="74.5" width="80" height="8" rx="6" fill="#F3F4F6" />
|
||||
<rect x="48" y="86.5" width="48" height="4" rx="6" fill="#F3F4F6" />
|
||||
<rect x="49" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="61" y="99.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||
<rect x="73" y="99.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
@@ -378,9 +378,9 @@ const embeds: {
|
||||
<rect x="85" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="97" y="161.5" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||
<rect x="109" y="161.5" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="24.5" y="51" width="139" height="163" rx="1.5" stroke="#292929" />
|
||||
<rect x="176" y="50.5" width="108" height="164" rx="2" fill="#E1E1E1" />
|
||||
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||
<rect x="24.5" y="51" width="139" height="163" rx="6" stroke="#292929" />
|
||||
<rect x="176" y="50.5" width="108" height="164" rx="6" fill="#F3F4F6" />
|
||||
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
@@ -400,12 +400,12 @@ const embeds: {
|
||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||
fill="white"
|
||||
/>
|
||||
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
||||
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
||||
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
|
||||
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||
<rect x="226" y="223.5" width="66" height="26" rx="2" fill="#292929" />
|
||||
<rect x="24" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||
<rect x="24" y="50.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
||||
<rect x="24" y="138.5" width="120" height="76" rx="6" fill="#F3F4F6" />
|
||||
<rect x="156" y="50.5" width="128" height="164" rx="6" fill="#F3F4F6" />
|
||||
<rect x="24" y="226.5" width="260" height="38.5" rx="6" fill="#F3F4F6" />
|
||||
<rect x="226" y="223.5" width="66" height="26" rx="6" fill="#292929" />
|
||||
<rect x="242" y="235.5" width="34" height="2" rx="1" fill="white" />
|
||||
</svg>
|
||||
),
|
||||
@@ -426,62 +426,65 @@ const embeds: {
|
||||
d="M0 1.99999C0 0.895423 0.895431 0 2 0H306C307.105 0 308 0.895431 308 2V263C308 264.105 307.105 265 306 265H2C0.895431 265 0 264.105 0 263V1.99999Z"
|
||||
fill="white"
|
||||
/>
|
||||
<rect x="24" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||
<rect x="24" y="50.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
||||
<rect x="24" y="138.5" width="120" height="76" rx="2" fill="#E1E1E1" />
|
||||
<rect x="156" y="50.5" width="128" height="164" rx="2" fill="#E1E1E1" />
|
||||
<rect x="24" y="226.5" width="260" height="38.5" rx="2" fill="#E1E1E1" />
|
||||
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" fill="#F8F8F8" />
|
||||
<rect opacity="0.8" x="108" y="85" width="80" height="8" rx="2" fill="#E1E1E1" />
|
||||
<rect x="108" y="97" width="48" height="4" rx="1" fill="#E1E1E1" />
|
||||
<rect x="109" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="121" y="110" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||
<rect x="133" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="145" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="157" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="169" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="181" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="193" y="110" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="145" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="157" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="169" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="181" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="193" y="124" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="109" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="121" y="136" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||
<path
|
||||
d="M121 135H127V133H121V135ZM128 136V142H130V136H128ZM127 143H121V145H127V143ZM120 142V136H118V142H120ZM121 143C120.448 143 120 142.552 120 142H118C118 143.657 119.343 145 121 145V143ZM128 142C128 142.552 127.552 143 127 143V145C128.657 145 130 143.657 130 142H128ZM127 135C127.552 135 128 135.448 128 136H130C130 134.343 128.657 133 127 133V135ZM121 133C119.343 133 118 134.343 118 136H120C120 135.448 120.448 135 121 135V133Z"
|
||||
fill="#3E3E3E"
|
||||
/>
|
||||
<rect x="133" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="145" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="157" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="169" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="181" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="193" y="136" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="109" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="121" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="133" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="145" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="157" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||
<rect x="169" y="148" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||
<rect x="181" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="193" y="148" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="109" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="121" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="133" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="145" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="157" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||
<rect x="169" y="160" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||
<rect x="181" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="193" y="160" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="109" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="121" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="133" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="145" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="157" y="172" width="6" height="6" rx="1" fill="#3E3E3E" />
|
||||
<rect x="169" y="172" width="6" height="6" rx="1" fill="#C6C6C6" />
|
||||
<rect x="84.5" y="61.5" width="139" height="141" rx="1.5" stroke="#292929" />
|
||||
<rect x="24" y="0.50293" width="260" height="24" rx="6" fill="#F3F4F6" />
|
||||
<rect x="24" y="35" width="259" height="192" rx="5.5" fill="#F9FAFB" />
|
||||
<g filter="url(#filter0_i_3223_14162)">
|
||||
<rect opacity="0.8" x="40" y="99" width="24" height="24" rx="2" fill="#E5E7EB" />
|
||||
<rect x="40" y="127" width="48" height="8" rx="1" fill="#E5E7EB" />
|
||||
<rect x="40" y="139" width="82" height="8" rx="1" fill="#E5E7EB" />
|
||||
<rect x="40" y="151" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||
<rect x="40" y="159" width="34" height="4" rx="1" fill="#E5E7EB" />
|
||||
</g>
|
||||
<rect x="152" y="48" width="2" height="169" rx="2" fill="#E5E7EB" />
|
||||
|
||||
<rect opacity="0.8" x="176" y="84" width="80" height="8" rx="2" fill="#E5E7EB" />
|
||||
<rect x="176" y="96" width="48" height="4" rx="1" fill="#E5E7EB" />
|
||||
<rect x="177" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="189" y="109" width="6" height="6" rx="1" fill="#0D121D" />
|
||||
<rect x="201" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="213" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="225" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="237" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="249" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="261" y="109" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="213" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="225" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="237" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="249" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="261" y="123" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="177" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="189" y="135" width="6" height="6" rx="1" fill="#0D121D" />
|
||||
<rect x="187.3" y="133.4" width="9" height="9" rx="1.5" stroke="#0D121D" />
|
||||
<rect x="201" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="213" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="225" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="237" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="249" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="261" y="135" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="177" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="189" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="201" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="213" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="225" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
||||
<rect x="237" y="147" width="6" height="6" rx="1" fill="#0D121D" />
|
||||
<rect x="249" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="261" y="147" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="177" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="189" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="201" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="213" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="225" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
||||
<rect x="237" y="159" width="6" height="6" rx="1" fill="#0D121D" />
|
||||
<rect x="249" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="261" y="159" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="177" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="189" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="201" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="213" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="225" y="171" width="6" height="6" rx="1" fill="#0D121D" />
|
||||
<rect x="237" y="171" width="6" height="6" rx="1" fill="#E5E7EB" />
|
||||
<rect x="24" y="35" width="259" height="192" rx="5.5" stroke="#101010" />
|
||||
<rect x="24" y="241.503" width="260" height="24" rx="6" fill="#F3F4F6" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
@@ -629,9 +632,9 @@ const ChooseEmbedTypesDialogContent = () => {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
return (
|
||||
<DialogContent type="creation" size="lg">
|
||||
<div className="mb-4">
|
||||
<h3 className="text-lg font-bold leading-6 text-gray-900" id="modal-title">
|
||||
<DialogContent className="rounded-lg p-10" type="creation" size="lg">
|
||||
<div className="mb-2">
|
||||
<h3 className="mb-2 text-2xl font-bold leading-none text-gray-900" id="modal-title">
|
||||
{t("how_you_want_add_cal_site", { appName: APP_NAME })}
|
||||
</h3>
|
||||
<div>
|
||||
@@ -641,7 +644,7 @@ const ChooseEmbedTypesDialogContent = () => {
|
||||
<div className="flex items-start">
|
||||
{embeds.map((embed, index) => (
|
||||
<button
|
||||
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-gray-100 ltr:mr-2 rtl:ml-2"
|
||||
className="hover:border-primary-900 w-1/3 rounded-md border border-gray-200 bg-gray-50 p-6 text-left hover:rounded-md hover:bg-gray-100 ltr:mr-4 ltr:last:mr-0 rtl:ml-4 rtl:last:ml-0"
|
||||
key={index}
|
||||
data-testid={embed.type}
|
||||
onClick={() => {
|
||||
@@ -649,11 +652,11 @@ const ChooseEmbedTypesDialogContent = () => {
|
||||
embedType: embed.type,
|
||||
});
|
||||
}}>
|
||||
<div className="order-none box-border flex-none rounded-sm border border-solid bg-white">
|
||||
<div className="order-none box-border flex-none rounded-md border border-solid bg-white">
|
||||
{embed.illustration}
|
||||
</div>
|
||||
<div className="mt-2 font-medium text-gray-900">{embed.title}</div>
|
||||
<p className="text-sm text-gray-500">{embed.subtitle}</p>
|
||||
<div className="mt-4 font-semibold leading-none text-gray-900">{embed.title}</div>
|
||||
<p className="mt-2 text-sm text-gray-500">{embed.subtitle}</p>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -795,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
}
|
||||
|
||||
const ThemeOptions = [
|
||||
{ value: Theme.auto, label: "Auto Theme" },
|
||||
{ value: Theme.auto, label: "Auto" },
|
||||
{ value: Theme.dark, label: "Dark Theme" },
|
||||
{ value: Theme.light, label: "Light Theme" },
|
||||
];
|
||||
@@ -812,11 +815,18 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
];
|
||||
|
||||
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 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
|
||||
className="h-6 w-6"
|
||||
onClick={() => {
|
||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||
}}>
|
||||
@@ -824,55 +834,43 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
</button>
|
||||
{embed.title}
|
||||
</h3>
|
||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
||||
<div className="flex flex-col overflow-y-auto">
|
||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||
<h4 className="mb-6 text-sm font-normal text-gray-500">{embed.subtitle}</h4>
|
||||
<div className="flex flex-col">
|
||||
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||
<Collapsible
|
||||
open={isEmbedCustomizationOpen}
|
||||
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">
|
||||
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
|
||||
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
|
||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||
<div className="text-sm">Embed Window Sizing</div>
|
||||
<div className="justify-left flex items-center">
|
||||
<TextField
|
||||
labelProps={{ className: "hidden" }}
|
||||
required
|
||||
value={previewState.inline.width}
|
||||
onChange={(e) => {
|
||||
setPreviewState((previewState) => {
|
||||
const width = e.target.value || "100%";
|
||||
<div className="mb-[9px] text-sm">Window sizing</div>
|
||||
<div className="justify-left flex items-center !font-normal">
|
||||
<div className="mr-[9px]">
|
||||
<TextField
|
||||
labelProps={{ className: "hidden" }}
|
||||
className="focus:ring-offset-0"
|
||||
required
|
||||
value={previewState.inline.width}
|
||||
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
|
||||
labelProps={{ className: "hidden" }}
|
||||
className="focus:ring-offset-0"
|
||||
value={previewState.inline.height}
|
||||
required
|
||||
onChange={(e) => {
|
||||
@@ -1004,22 +1002,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</div>
|
||||
<hr className="mt-4" />
|
||||
<div className="mt-4 font-medium">
|
||||
<div className="font-medium">
|
||||
<Collapsible
|
||||
open={isBookingCustomizationOpen}
|
||||
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>
|
||||
<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
|
||||
checked={previewState.hideEventTypeDetails}
|
||||
onCheckedChange={(checked) => {
|
||||
@@ -1033,13 +1022,38 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
/>
|
||||
<div className="text-sm">{t("hide_eventtype_details")}</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>
|
||||
<Select
|
||||
className="w-full"
|
||||
defaultValue={ThemeOptions[0]}
|
||||
components={{
|
||||
Control: ThemeSelectControl,
|
||||
IndicatorSeparator: () => null,
|
||||
}}
|
||||
onChange={(option) => {
|
||||
if (!option) {
|
||||
@@ -1055,30 +1069,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
options={ThemeOptions}
|
||||
/>
|
||||
</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>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AdminRequired } from "components/ui/AdminRequired";
|
||||
import noop from "lodash/noop";
|
||||
import { noop } from "lodash";
|
||||
import type { LinkProps } from "next/link";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
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, Select } from "@calcom/ui";
|
||||
import {
|
||||
Button,
|
||||
Dropdown,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuItem,
|
||||
DropdownItem,
|
||||
} from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
|
||||
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 { t } = useLocale();
|
||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
type: "new_other",
|
||||
});
|
||||
return (
|
||||
<Select
|
||||
name="additionalCalendar"
|
||||
placeholder={
|
||||
<Button StartIcon={FiPlus} color="secondary">
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
}
|
||||
options={options}
|
||||
isSearchable={false}
|
||||
isLoading={isLoading}
|
||||
components={{ Option: ImageOption }}
|
||||
styles={{
|
||||
menu: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
"@media only screen and (max-width: 640px)": {
|
||||
left: "0",
|
||||
},
|
||||
width: "max-content",
|
||||
right: "0",
|
||||
}),
|
||||
indicatorSeparator: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
display: "none",
|
||||
}),
|
||||
control: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
padding: "0",
|
||||
border: "0",
|
||||
borderRadius: "6px",
|
||||
minHeight: "auto",
|
||||
}),
|
||||
dropdownIndicator: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
display: "none",
|
||||
}),
|
||||
valueContainer: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
padding: "0",
|
||||
}),
|
||||
placeholder: (defaultStyles) => ({
|
||||
...defaultStyles,
|
||||
margin: "0",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{options.map((data) => (
|
||||
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||
{data.slug === "add-new" ? (
|
||||
<DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
|
||||
{t("install_new_calendar_app")}
|
||||
</DropdownItem>
|
||||
) : (
|
||||
<InstallAppButton
|
||||
type={data.type}
|
||||
render={(installProps) => {
|
||||
const props = { ...installProps } as any;
|
||||
return (
|
||||
<DropdownItem {...props} color="minimal" type="button">
|
||||
<span className="flex items-center gap-x-2">
|
||||
{data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
|
||||
{`${t("add")} ${data.label}`}
|
||||
</span>
|
||||
</DropdownItem>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import type { IframeHTMLAttributes } from "react";
|
||||
import React, { useState } from "react";
|
||||
|
||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||
@@ -45,12 +46,12 @@ const Component = ({
|
||||
tos,
|
||||
privacy,
|
||||
isProOnly,
|
||||
images,
|
||||
descriptionItems,
|
||||
isTemplate,
|
||||
dependencies,
|
||||
}: Parameters<typeof App>[0]) => {
|
||||
const { t } = useLocale();
|
||||
const hasImages = images && images.length > 0;
|
||||
const hasDescriptionItems = descriptionItems && descriptionItems.length > 0;
|
||||
const router = useRouter();
|
||||
|
||||
const mutation = useAddAppMutation(null, {
|
||||
@@ -94,17 +95,25 @@ const Component = ({
|
||||
|
||||
return (
|
||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||
{hasImages && (
|
||||
{hasDescriptionItems && (
|
||||
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||
{images ? (
|
||||
images.map((img) => (
|
||||
<img
|
||||
key={img}
|
||||
src={img}
|
||||
alt={`Screenshot of app ${name}`}
|
||||
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
|
||||
/>
|
||||
))
|
||||
{descriptionItems ? (
|
||||
descriptionItems.map((descriptionItem, index) =>
|
||||
typeof descriptionItem === "object" ? (
|
||||
<div
|
||||
key={`iframe-${index}`}
|
||||
className="mr-4 max-h-full min-h-[315px] min-w-[90%] max-w-full snap-center last:mb-0 lg:mb-4 lg:mr-0 [&_iframe]:h-full [&_iframe]:min-h-[315px] [&_iframe]:w-full">
|
||||
<iframe allowFullScreen {...descriptionItem.iframe} />
|
||||
</div>
|
||||
) : (
|
||||
<img
|
||||
key={descriptionItem}
|
||||
src={descriptionItem}
|
||||
alt={`Screenshot of app ${name}`}
|
||||
className="mr-4 h-auto max-h-80 max-w-[90%] snap-center rounded-md object-contain last:mb-0 md:max-h-min lg:mb-4 lg:mr-0 lg:max-w-full"
|
||||
/>
|
||||
)
|
||||
)
|
||||
) : (
|
||||
<SkeletonText />
|
||||
)}
|
||||
@@ -113,7 +122,7 @@ const Component = ({
|
||||
<div
|
||||
className={classNames(
|
||||
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
|
||||
hasImages && "lg:ml-8"
|
||||
hasDescriptionItems && "lg:ml-8"
|
||||
)}>
|
||||
<div className="mb-8 flex pt-4">
|
||||
<header>
|
||||
@@ -354,7 +363,7 @@ export default function App(props: {
|
||||
privacy?: string;
|
||||
licenseRequired: AppType["licenseRequired"];
|
||||
isProOnly: AppType["isProOnly"];
|
||||
images?: string[];
|
||||
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
|
||||
isTemplate?: boolean;
|
||||
disableInstall?: boolean;
|
||||
dependencies?: string[];
|
||||
|
||||
@@ -1,149 +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) }),
|
||||
option: (defaultStyles, state) => ({
|
||||
...defaultStyles,
|
||||
backgroundColor: state.isSelected
|
||||
? state.isFocused
|
||||
? "var(--brand-color)"
|
||||
: "var(--brand-color)"
|
||||
: state.isFocused
|
||||
? "var(--brand-color-dark-mode)"
|
||||
: "var(--brand-text-color)",
|
||||
}),
|
||||
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";
|
||||
|
||||
const excludeNullValues = (value: unknown) => !!value;
|
||||
|
||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return locations.length ? (
|
||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
||||
{locations.map((location, index) => {
|
||||
const eventLocationType = getEventLocationType(location.type);
|
||||
if (!eventLocationType) {
|
||||
// It's possible that the location app got uninstalled
|
||||
return null;
|
||||
}
|
||||
const renderLocations = locations.map((location, index) => {
|
||||
const eventLocationType = getEventLocationType(location.type);
|
||||
if (!eventLocationType) {
|
||||
// It's possible that the location app got uninstalled
|
||||
return null;
|
||||
}
|
||||
if (eventLocationType.variable === "hostDefault") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const translateAbleKeys = [
|
||||
"attendee_in_person",
|
||||
"in_person",
|
||||
"attendee_phone_number",
|
||||
"link_meeting",
|
||||
"organizer_phone_number",
|
||||
];
|
||||
const translateAbleKeys = [
|
||||
"attendee_in_person",
|
||||
"in_person",
|
||||
"attendee_phone_number",
|
||||
"link_meeting",
|
||||
"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:")
|
||||
? eventLocationType.label
|
||||
: translateAbleKeys.includes(locationKey)
|
||||
? t(locationKey)
|
||||
: locationKey;
|
||||
|
||||
return (
|
||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||
<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 " />
|
||||
) : (
|
||||
<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`}
|
||||
/>
|
||||
return (
|
||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||
<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 " />
|
||||
) : (
|
||||
<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]" : ""
|
||||
)}
|
||||
<Tooltip content={translatedLocation}>
|
||||
<p className="line-clamp-1">{translatedLocation}</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
alt={`${eventLocationType.label} icon`}
|
||||
/>
|
||||
)}
|
||||
<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>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
slots.map((slot) => {
|
||||
type BookingURL = {
|
||||
pathname: string;
|
||||
query: Record<string, string | number | string[] | undefined>;
|
||||
query: Record<string, string | number | string[] | undefined | TimeFormat>;
|
||||
};
|
||||
const bookingUrl: BookingURL = {
|
||||
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
||||
@@ -103,6 +103,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
date: dayjs.utc(slot.time).tz(timeZone()).format(),
|
||||
type: eventTypeId,
|
||||
slug: eventTypeSlug,
|
||||
timeFormat,
|
||||
/** Treat as recurring only when a count exist and it's not a rescheduling workflow */
|
||||
count: recurringCount && !rescheduleUid ? recurringCount : undefined,
|
||||
...(ethSignature ? { ethSignature } : {}),
|
||||
|
||||
@@ -135,7 +135,6 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
<li key={idx}>
|
||||
<Badge
|
||||
variant="gray"
|
||||
size="lg"
|
||||
className={classNames(
|
||||
duration === dur.toString()
|
||||
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useState } from "react";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
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 classNames from "@calcom/lib/classNames";
|
||||
import { formatTime } from "@calcom/lib/date-fns";
|
||||
@@ -46,7 +48,10 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
// Get user so we can determine 12/24 hour format preferences
|
||||
const query = useMeQuery();
|
||||
const user = query.data;
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
t,
|
||||
i18n: { language },
|
||||
} = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const router = useRouter();
|
||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||
@@ -195,7 +200,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
);
|
||||
};
|
||||
|
||||
const startTime = dayjs(booking.startTime).format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
||||
const startTime = dayjs(booking.startTime)
|
||||
.locale(language)
|
||||
.format(isUpcoming ? "ddd, D MMM" : "D MMMM YYYY");
|
||||
const [isOpenRescheduleDialog, setIsOpenRescheduleDialog] = useState(false);
|
||||
const [isOpenSetLocationDialog, setIsOpenLocationDialog] = useState(false);
|
||||
const setLocationMutation = trpc.viewer.bookings.editLocation.useMutation({
|
||||
@@ -431,7 +438,10 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
||||
// Get user so we can determine 12/24 hour format preferences
|
||||
const query = useMeQuery();
|
||||
const user = query.data;
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
t,
|
||||
i18n: { language },
|
||||
} = useLocale();
|
||||
const now = new Date();
|
||||
const recurringCount = recurringDates.filter((date) => {
|
||||
return (
|
||||
@@ -461,7 +471,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
||||
<p key={key} className={classNames(pastOrCancelled && "line-through")}>
|
||||
{formatTime(aDate, user?.timeFormat, user?.timeZone)}
|
||||
{" - "}
|
||||
{dayjs(aDate).format("D MMMM YYYY")}
|
||||
{dayjs(aDate).locale(language).format("D MMMM YYYY")}
|
||||
</p>
|
||||
);
|
||||
})}>
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function CancelBooking(props: Props) {
|
||||
{t("nevermind")}
|
||||
</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"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
@@ -86,7 +86,7 @@ export default function CancelBooking(props: Props) {
|
||||
|
||||
const res = await fetch("/api/cancel", {
|
||||
body: JSON.stringify({
|
||||
id: booking?.id,
|
||||
uid: booking?.uid,
|
||||
cancellationReason: cancellationReason,
|
||||
allRemainingBookings,
|
||||
// @NOTE: very important this shouldn't cancel with number ID use uid instead
|
||||
|
||||
@@ -133,10 +133,7 @@ export const SlotPicker = ({
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
usernameList: users,
|
||||
startTime:
|
||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
||||
: browsingDate?.startOf("month"),
|
||||
startTime: dayjs().startOf("day"),
|
||||
endTime: browsingDate?.endOf("month"),
|
||||
timeZone,
|
||||
duration,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import type { ITimezoneOption } from "@calcom/ui";
|
||||
import { TimezoneSelect } from "@calcom/ui";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import { timeZone } from "../../lib/clock";
|
||||
|
||||
type Props = {
|
||||
@@ -15,8 +12,6 @@ type Props = {
|
||||
|
||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||
const query = useMeQuery();
|
||||
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTimeZone(timeZone());
|
||||
@@ -28,59 +23,16 @@ const TimeOptions: FC<Props> = ({ 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 ? (
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
classNames={{
|
||||
singleValue: () => "dark:text-darkgray-600 text-gray-600",
|
||||
menu: () => "!w-64 max-w-[90vw]",
|
||||
}}
|
||||
variant="minimal"
|
||||
value={selectedTimeZone}
|
||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||
className="flex h-6 text-sm font-medium"
|
||||
styles={customStyles}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -17,8 +17,8 @@ export function TimezoneDropdown({
|
||||
|
||||
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">
|
||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
||||
<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-2 rtl:ml-2" />
|
||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -154,6 +154,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
nofollow: eventType.hidden,
|
||||
noindex: eventType.hidden,
|
||||
}}
|
||||
isBrandingHidden={restProps.isBrandingHidden}
|
||||
/>
|
||||
<BookingPageTagManager eventType={eventType} />
|
||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useSession } from "next-auth/react";
|
||||
import dynamic from "next/dynamic";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useCallback, useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { useForm, useFormContext } from "react-hook-form";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
@@ -15,7 +15,6 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
|
||||
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
|
||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||
import type { LocationObject } from "@calcom/core/location";
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
useEmbedNonStylesConfig,
|
||||
@@ -36,19 +35,19 @@ import classNames from "@calcom/lib/classNames";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { Button, Form, Tooltip } from "@calcom/ui";
|
||||
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||
import { parseRecurringDates, parseDate } from "@lib/parseDate";
|
||||
|
||||
import type { Gate, GateState } from "@components/Gates";
|
||||
import Gates from "@components/Gates";
|
||||
@@ -58,6 +57,8 @@ import type { BookPageProps } from "../../../pages/[user]/book";
|
||||
import type { HashLinkPageProps } from "../../../pages/d/[link]/book";
|
||||
import type { TeamBookingPageProps } from "../../../pages/team/[slug]/book";
|
||||
|
||||
const Toaster = dynamic(() => import("react-hot-toast").then((mod) => mod.Toaster), { ssr: false });
|
||||
|
||||
/** These are like 40kb that not every user needs */
|
||||
const BookingDescriptionPayment = dynamic(
|
||||
() => import("@components/booking/BookingDescriptionPayment")
|
||||
@@ -78,6 +79,7 @@ const BookingFields = ({
|
||||
const { t } = useLocale();
|
||||
const { watch, setValue } = useFormContext();
|
||||
const locationResponse = watch("responses.location");
|
||||
const currentView = rescheduleUid ? "reschedule" : "";
|
||||
|
||||
return (
|
||||
// TODO: It might make sense to extract this logic into BookingFields config, that would allow to quickly configure system fields and their editability in fresh booking and reschedule booking view
|
||||
@@ -87,12 +89,16 @@ const BookingFields = ({
|
||||
// Allowing a system field to be edited might require sending emails to attendees, so we need to be careful
|
||||
let readOnly =
|
||||
(field.editable === "system" || field.editable === "system-but-optional") && !!rescheduleUid;
|
||||
|
||||
let noLabel = false;
|
||||
let hidden = !!field.hidden;
|
||||
const fieldViews = field.views;
|
||||
|
||||
if (fieldViews && !fieldViews.find((view) => view.id === currentView)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (field.name === SystemField.Enum.rescheduleReason) {
|
||||
if (!rescheduleUid) {
|
||||
return null;
|
||||
}
|
||||
// rescheduleReason is a reschedule specific field and thus should be editable during reschedule
|
||||
readOnly = false;
|
||||
}
|
||||
@@ -180,9 +186,26 @@ const BookingFields = ({
|
||||
);
|
||||
};
|
||||
|
||||
const routerQuerySchema = z
|
||||
.object({
|
||||
timeFormat: z.nativeEnum(TimeFormat),
|
||||
rescheduleUid: z.string().optional(),
|
||||
date: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((date) => {
|
||||
if (date === undefined) {
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
}),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
const BookingPage = ({
|
||||
eventType,
|
||||
booking,
|
||||
currentSlotBooking,
|
||||
profile,
|
||||
isDynamicGroupBooking,
|
||||
recurringEventCount,
|
||||
@@ -218,20 +241,6 @@ const BookingPage = ({
|
||||
duration = Number(queryDuration);
|
||||
}
|
||||
|
||||
// This is a workaround for forcing the same time format for both server side rendering and client side rendering
|
||||
// At initial render, we use the default time format which is 12H
|
||||
const [withDefaultTimeFormat, setWithDefaultTimeFormat] = useState(true);
|
||||
const parseDateFunc = useCallback(
|
||||
(date: string | null | Dayjs) => {
|
||||
return parseDate(date, i18n, withDefaultTimeFormat);
|
||||
},
|
||||
[withDefaultTimeFormat]
|
||||
);
|
||||
// After intial render on client side, we let parseDateFunc to use the time format from the localStorage
|
||||
useEffect(() => {
|
||||
setWithDefaultTimeFormat(false);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (top !== window) {
|
||||
//page_view will be collected automatically by _middleware.ts
|
||||
@@ -279,6 +288,7 @@ const BookingPage = ({
|
||||
return router.push({
|
||||
pathname: `/booking/${uid}`,
|
||||
query: {
|
||||
isSuccessBookingPage: true,
|
||||
allRemainingBookings: true,
|
||||
email: bookingForm.getValues("responses.email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
@@ -288,11 +298,17 @@ const BookingPage = ({
|
||||
},
|
||||
});
|
||||
|
||||
const rescheduleUid = router.query.rescheduleUid as string;
|
||||
const {
|
||||
data: { timeFormat, rescheduleUid, date },
|
||||
} = useTypedQuery(routerQuerySchema);
|
||||
|
||||
useTheme(profile.theme);
|
||||
const date = asStringOrNull(router.query.date);
|
||||
|
||||
const querySchema = getBookingResponsesPartialSchema({
|
||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||
eventType: {
|
||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||
},
|
||||
view: rescheduleUid ? "reschedule" : "booking",
|
||||
});
|
||||
|
||||
const parsedQuery = querySchema.parse({
|
||||
@@ -372,7 +388,8 @@ const BookingPage = ({
|
||||
const bookingFormSchema = z
|
||||
.object({
|
||||
responses: getBookingResponsesSchema({
|
||||
bookingFields: getBookingFieldsWithSystemFields(eventType),
|
||||
eventType: { bookingFields: getBookingFieldsWithSystemFields(eventType) },
|
||||
view: rescheduleUid ? "reschedule" : "booking",
|
||||
}),
|
||||
})
|
||||
.passthrough();
|
||||
@@ -390,26 +407,17 @@ const BookingPage = ({
|
||||
// Calculate the booking date(s)
|
||||
let recurringStrings: string[] = [],
|
||||
recurringDates: Date[] = [];
|
||||
const parseRecurringDatesFunc = useCallback(
|
||||
(date: string | null | Dayjs, recurringEvent: RecurringEvent, recurringCount: number) => {
|
||||
return parseRecurringDates(
|
||||
{
|
||||
startDate: date,
|
||||
timeZone: timeZone(),
|
||||
recurringEvent: recurringEvent,
|
||||
recurringCount: recurringCount,
|
||||
withDefaultTimeFormat: withDefaultTimeFormat,
|
||||
},
|
||||
i18n
|
||||
);
|
||||
},
|
||||
[withDefaultTimeFormat, date, eventType.recurringEvent, recurringEventCount]
|
||||
);
|
||||
|
||||
if (eventType.recurringEvent?.freq && recurringEventCount !== null) {
|
||||
[recurringStrings, recurringDates] = parseRecurringDatesFunc(
|
||||
date,
|
||||
eventType.recurringEvent,
|
||||
parseInt(recurringEventCount.toString())
|
||||
[recurringStrings, recurringDates] = parseRecurringDates(
|
||||
{
|
||||
startDate: date,
|
||||
timeZone: timeZone(),
|
||||
recurringEvent: eventType.recurringEvent,
|
||||
recurringCount: parseInt(recurringEventCount.toString()),
|
||||
selectedTimeFormat: timeFormat,
|
||||
},
|
||||
i18n
|
||||
);
|
||||
}
|
||||
|
||||
@@ -437,8 +445,8 @@ const BookingPage = ({
|
||||
const recurringEventId = uuidv4();
|
||||
const recurringBookings = recurringDates.map((recurringDate) => ({
|
||||
...bookingValues,
|
||||
start: dayjs(recurringDate).format(),
|
||||
end: dayjs(recurringDate).add(duration, "minute").format(),
|
||||
start: dayjs(recurringDate).utc().format(),
|
||||
end: dayjs(recurringDate).utc().add(duration, "minute").format(),
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
recurringEventId,
|
||||
@@ -457,8 +465,8 @@ const BookingPage = ({
|
||||
} else {
|
||||
mutation.mutate({
|
||||
...bookingValues,
|
||||
start: dayjs(date).tz(timeZone()).format(),
|
||||
end: dayjs(date).tz(timeZone()).add(duration, "minute").format(),
|
||||
start: dayjs(date).utc().format(),
|
||||
end: dayjs(date).utc().add(duration, "minute").format(),
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
timeZone: timeZone(),
|
||||
@@ -539,7 +547,7 @@ const BookingPage = ({
|
||||
<div className="text-sm font-medium">
|
||||
{isClientTimezoneAvailable &&
|
||||
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||
`${parseDateFunc(date)}`}
|
||||
`${parseDate(date, i18n, timeFormat)}`}
|
||||
{isClientTimezoneAvailable &&
|
||||
!rescheduleUid &&
|
||||
eventType.recurringEvent?.freq &&
|
||||
@@ -569,7 +577,7 @@ const BookingPage = ({
|
||||
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{isClientTimezoneAvailable &&
|
||||
typeof booking.startTime === "string" &&
|
||||
parseDateFunc(dayjs(booking.startTime))}
|
||||
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -577,23 +585,27 @@ const BookingPage = ({
|
||||
<div className="text-bookinghighlight flex items-start text-sm">
|
||||
<FiUser
|
||||
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"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
: currentSlotBooking &&
|
||||
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-bookinghighlight"
|
||||
}`}
|
||||
/>
|
||||
<p
|
||||
className={`${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
currentSlotBooking &&
|
||||
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
: currentSlotBooking &&
|
||||
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-bookinghighlight"
|
||||
} mb-2 font-medium`}>
|
||||
{booking
|
||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||
{currentSlotBooking
|
||||
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
|
||||
: eventType.seatsPerTimeSlot}{" "}
|
||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
@@ -625,6 +637,7 @@ const BookingPage = ({
|
||||
</Button>
|
||||
<Button
|
||||
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"}
|
||||
loading={mutation.isLoading || recurringMutation.isLoading}>
|
||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||
@@ -638,6 +651,7 @@ const BookingPage = ({
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Toaster position="bottom-right" />
|
||||
</Gates>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Input } from "@calcom/ui";
|
||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||
import { FiMapPin } from "@calcom/ui/components/icon";
|
||||
|
||||
@@ -32,6 +33,7 @@ interface ISetLocationDialog {
|
||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
||||
selection?: LocationOption;
|
||||
booking?: BookingItem;
|
||||
isTeamEvent?: boolean;
|
||||
defaultValues?: LocationObject[];
|
||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isOpenDialog: boolean;
|
||||
@@ -52,7 +54,7 @@ const LocationInput = (props: {
|
||||
const { control } = useFormContext() as typeof locationFormMethods;
|
||||
if (eventLocationType?.organizerInputType === "text") {
|
||||
return (
|
||||
<input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
||||
<Input {...locationFormMethods.register(eventLocationType.variable)} type="text" {...remainingProps} />
|
||||
);
|
||||
} else if (eventLocationType?.organizerInputType === "phone") {
|
||||
return (
|
||||
@@ -73,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
saveLocation,
|
||||
selection,
|
||||
booking,
|
||||
isTeamEvent,
|
||||
setShowLocationModal,
|
||||
isOpenDialog,
|
||||
defaultValues,
|
||||
@@ -171,7 +174,6 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
id="locationInput"
|
||||
placeholder={t(eventLocationType.organizerInputPlaceholder || "")}
|
||||
required
|
||||
className="block w-full rounded-sm border-gray-300 text-sm"
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
defaultValue={
|
||||
defaultLocation ? defaultLocation[eventLocationType.defaultValueVariable] : undefined
|
||||
@@ -290,7 +292,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
query={locationsQuery}
|
||||
success={({ data }) => {
|
||||
if (!data.length) return null;
|
||||
const locationOptions = [...data];
|
||||
const locationOptions = [...data].filter((option) => {
|
||||
return !isTeamEvent ? option.label !== "Conferencing" : true;
|
||||
});
|
||||
if (booking) {
|
||||
locationOptions.map((location) =>
|
||||
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 dayjs from "@calcom/dayjs";
|
||||
import { NewScheduleButton } from "@calcom/features/schedules";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
||||
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
@@ -52,23 +54,24 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||
|
||||
const AvailabilitySelect = ({
|
||||
className = "",
|
||||
isLoading,
|
||||
schedules,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
name: string;
|
||||
value: number;
|
||||
isLoading: boolean;
|
||||
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
|
||||
onBlur: () => void;
|
||||
onChange: (value: AvailabilityOption | null) => void;
|
||||
}) => {
|
||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||
const { t } = useLocale();
|
||||
|
||||
if (isLoading) {
|
||||
return <SelectSkeletonLoader />;
|
||||
}
|
||||
|
||||
const schedules = data?.schedules || [];
|
||||
|
||||
const options = schedules.map((schedule) => ({
|
||||
value: schedule.id,
|
||||
label: schedule.name,
|
||||
@@ -107,6 +110,7 @@ const EventTypeScheduleDetails = () => {
|
||||
const { watch } = useFormContext<FormValues>();
|
||||
|
||||
const scheduleId = watch("schedule");
|
||||
|
||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
||||
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
||||
@@ -174,6 +178,19 @@ const EventTypeScheduleDetails = () => {
|
||||
|
||||
const EventTypeSchedule = () => {
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@@ -183,14 +200,18 @@ const EventTypeSchedule = () => {
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => (
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
schedules={schedules?.schedules || []}
|
||||
isLoading={isLoading}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,11 +10,12 @@ import type { EventNameObjectType } from "@calcom/core/event";
|
||||
import { getEventName } from "@calcom/core/event";
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
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 type { Prisma } from "@calcom/prisma/client";
|
||||
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 RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
@@ -183,9 +184,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
setRedirectUrlVisible(e);
|
||||
onChange(e ? value : "");
|
||||
}}>
|
||||
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
||||
<div className="lg:-ml-2">
|
||||
{/* Textfield has some margin by default we remove that so we can keep consistent alignment */}
|
||||
<div className="lg:-ml-2 lg:-mb-2">
|
||||
<TextField
|
||||
className="w-full"
|
||||
label={t("redirect_success_booking")}
|
||||
labelSrOnly
|
||||
placeholder={t("external_redirect_url")}
|
||||
@@ -194,21 +196,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
defaultValue={eventType.successRedirectUrl || ""}
|
||||
{...formMethods.register("successRedirectUrl")}
|
||||
/>
|
||||
<div className="mt-2 flex">
|
||||
<Checkbox
|
||||
description={t("disable_success_page")}
|
||||
// Disable if it's not Self Hosted or if the redirect url is not set
|
||||
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
|
||||
{...formMethods.register("metadata.disableSuccessPage")}
|
||||
/>
|
||||
{/*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
|
||||
className={classNames(
|
||||
"p-1 text-sm text-orange-600",
|
||||
formMethods.getValues("successRedirectUrl") ? "block" : "hidden"
|
||||
)}>
|
||||
{t("redirect_url_warning")}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
|
||||
@@ -88,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
label={t("minimum_booking_notice")}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
min={0}
|
||||
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
||||
/>
|
||||
<input type="hidden" ref={ref} {...passThroughProps} />
|
||||
@@ -424,7 +425,8 @@ const IntervalLimitItem = ({
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
containerClassName={`${textFieldSuffix ? "w-36" : "w-16"} -mb-1`}
|
||||
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
||||
className="mb-0"
|
||||
placeholder={`${value}`}
|
||||
min={step}
|
||||
step={step}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
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 type { MultiValue } from "react-select";
|
||||
import { z } from "zod";
|
||||
@@ -16,7 +16,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { slugify } from "@calcom/lib/slugify";
|
||||
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 { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
@@ -35,14 +45,31 @@ const getLocationFromType = (
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
||||
for (const locationType of options) {
|
||||
for (const location of locationType.options) {
|
||||
if (location.value === type && location.disabled === false) {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
}
|
||||
interface DescriptionEditorProps {
|
||||
description?: string | null;
|
||||
}
|
||||
|
||||
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 = (
|
||||
@@ -53,12 +80,16 @@ export const EventSetupTab = (
|
||||
) => {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { eventType, locationOptions, team, destinationCalendar } = props;
|
||||
const { eventType, team, destinationCalendar } = props;
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||
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) => ({
|
||||
value: mins,
|
||||
label: t("multiple_duration_mins", { count: mins }),
|
||||
@@ -199,7 +230,7 @@ export const EventSetupTab = (
|
||||
return (
|
||||
<li
|
||||
key={`${location.type}${index}`}
|
||||
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||
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">
|
||||
<img
|
||||
@@ -279,12 +310,7 @@ export const EventSetupTab = (
|
||||
/>
|
||||
<div>
|
||||
<Label>{t("description")}</Label>
|
||||
<Editor
|
||||
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
/>
|
||||
<DescriptionEditor description={eventType?.description} />
|
||||
</div>
|
||||
<TextField
|
||||
required
|
||||
@@ -401,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 */}
|
||||
<EditLocationDialog
|
||||
isTeamEvent={!!team}
|
||||
isOpenDialog={showLocationModal}
|
||||
setShowLocationModal={setShowLocationModal}
|
||||
saveLocation={saveLocation}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export type EventTypeDescriptionSafeProps = {
|
||||
eventType: { description: string | null };
|
||||
eventType: { description: string | null; descriptionAsSafeHTML: string | null };
|
||||
};
|
||||
|
||||
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
||||
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
||||
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
|
||||
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
|
||||
if (eventType.description)
|
||||
props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
|
||||
return <div {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ function EventTypeSingleLayout({
|
||||
tooltip={t("embed")}
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
color="destructive"
|
||||
variant="icon"
|
||||
StartIcon={FiTrash}
|
||||
tooltip={t("delete")}
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterInputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
|
||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
||||
|
||||
interface ICreateEventsOnCalendarSelectProps {
|
||||
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/outline";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
@@ -22,23 +25,30 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
const { t } = useLocale();
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState(dayjs.tz.guess());
|
||||
const telemetry = useTelemetry();
|
||||
const userSettingsSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
|
||||
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
|
||||
}),
|
||||
});
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm({
|
||||
} = useForm<z.infer<typeof userSettingsSchema>>({
|
||||
defaultValues: {
|
||||
name: user?.name || "",
|
||||
},
|
||||
reValidateMode: "onChange",
|
||||
resolver: zodResolver(userSettingsSchema),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
telemetry.event(telemetryEventTypes.onboardingStarted);
|
||||
}, [telemetry]);
|
||||
|
||||
const defaultOptions = { required: true, maxLength: 255 };
|
||||
|
||||
const utils = trpc.useContext();
|
||||
const onSuccess = async () => {
|
||||
await utils.viewer.me.invalidate();
|
||||
@@ -67,7 +77,9 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
{t("full_name")}
|
||||
</label>
|
||||
<input
|
||||
{...register("name", defaultOptions)}
|
||||
{...register("name", {
|
||||
required: true,
|
||||
})}
|
||||
id="name"
|
||||
name="name"
|
||||
type="text"
|
||||
@@ -77,7 +89,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
||||
/>
|
||||
{errors.name && (
|
||||
<p data-testid="required" className="py-2 text-xs text-red-500">
|
||||
{t("required")}
|
||||
{errors.name.message}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -75,7 +75,9 @@ export function CalendarSwitch(props: {
|
||||
/>
|
||||
{props.defaultSelected && (
|
||||
<Badge variant="gray">
|
||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
<div className="flex">
|
||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
</div>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,14 @@ import Link from "next/link";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
type TeamType = NonNullable<TeamWithMembers>;
|
||||
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 { t } = useLocale();
|
||||
@@ -30,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
<>
|
||||
<div
|
||||
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) {
|
||||
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 (
|
||||
<div>
|
||||
<Members members={team.members} teamName={team.name} />
|
||||
|
||||
@@ -3,8 +3,7 @@ import { components } from "react-select";
|
||||
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
import { Select } from "@calcom/ui";
|
||||
|
||||
export type LocationOption = {
|
||||
label: string;
|
||||
@@ -17,21 +16,11 @@ export type SingleValueLocationOption = SingleValue<LocationOption>;
|
||||
|
||||
export type GroupOptionType = GroupBase<LocationOption>;
|
||||
|
||||
const OptionWithIcon = ({
|
||||
icon,
|
||||
isSelected,
|
||||
label,
|
||||
}: {
|
||||
icon?: string;
|
||||
isSelected?: boolean;
|
||||
label: string;
|
||||
}) => {
|
||||
const OptionWithIcon = ({ icon, label }: { icon?: string; label: string }) => {
|
||||
return (
|
||||
<div className="flex items-center gap-3">
|
||||
{icon && <img src={icon} alt="cover" className="h-3.5 w-3.5" />}
|
||||
<span className={classNames("text-sm font-medium", isSelected ? "text-white" : "text-gray-900")}>
|
||||
{label}
|
||||
</span>
|
||||
<span className={classNames("text-sm font-medium")}>{label}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -44,7 +33,7 @@ export default function LocationSelect(props: Props<LocationOption, false, Group
|
||||
components={{
|
||||
Option: (props) => (
|
||||
<components.Option {...props}>
|
||||
<OptionWithIcon icon={props.data.icon} label={props.data.label} isSelected={props.isSelected} />
|
||||
<OptionWithIcon icon={props.data.icon} label={props.data.label} />
|
||||
</components.Option>
|
||||
),
|
||||
SingleValue: (props) => (
|
||||
|
||||
@@ -10,6 +10,8 @@ import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/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 { MetaProvider } from "@calcom/ui";
|
||||
|
||||
@@ -58,6 +60,11 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
||||
return <I18nextAdapter {...passedProps} />;
|
||||
};
|
||||
|
||||
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||
const flags = useFlags();
|
||||
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||
}
|
||||
|
||||
const AppProviders = (props: AppPropsWithChildren) => {
|
||||
const session = trpc.viewer.public.session.useQuery().data;
|
||||
// No need to have intercom on public pages - Good for Page Performance
|
||||
@@ -85,7 +92,9 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
storageKey={storageKey}
|
||||
forcedTheme={forcedTheme}
|
||||
attribute="class">
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
<FeatureFlagsProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</ThemeProvider>
|
||||
</TooltipProvider>
|
||||
</CustomI18nextProvider>
|
||||
|
||||
@@ -3,23 +3,22 @@ import { RRule } from "rrule";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { detectBrowserTimeFormat, TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { parseZone } from "./parseZone";
|
||||
|
||||
const processDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
|
||||
const processDate = (date: string | null | Dayjs, i18n: I18n, selectedTimeFormat?: TimeFormat) => {
|
||||
const parsedZone = parseZone(date);
|
||||
if (!parsedZone?.isValid()) return "Invalid date";
|
||||
const formattedTime = parsedZone?.format(
|
||||
withDefaultTimeFormat ? TimeFormat.TWELVE_HOUR : detectBrowserTimeFormat
|
||||
);
|
||||
const formattedTime = parsedZone?.format(selectedTimeFormat || detectBrowserTimeFormat);
|
||||
return formattedTime + ", " + dayjs(date).toDate().toLocaleString(i18n.language, { dateStyle: "full" });
|
||||
};
|
||||
|
||||
export const parseDate = (date: string | null | Dayjs, i18n: I18n, withDefaultTimeFormat: boolean) => {
|
||||
export const parseDate = (date: string | null | Dayjs, i18n: I18n, selectedTimeFormat?: TimeFormat) => {
|
||||
if (!date) return ["No date"];
|
||||
return processDate(date, i18n, withDefaultTimeFormat);
|
||||
return processDate(date, i18n, selectedTimeFormat);
|
||||
};
|
||||
|
||||
export const parseRecurringDates = (
|
||||
@@ -28,13 +27,13 @@ export const parseRecurringDates = (
|
||||
timeZone,
|
||||
recurringEvent,
|
||||
recurringCount,
|
||||
withDefaultTimeFormat,
|
||||
selectedTimeFormat,
|
||||
}: {
|
||||
startDate: string | null | Dayjs;
|
||||
timeZone?: string;
|
||||
recurringEvent: RecurringEvent | null;
|
||||
recurringCount: number;
|
||||
withDefaultTimeFormat: boolean;
|
||||
selectedTimeFormat?: TimeFormat;
|
||||
},
|
||||
i18n: I18n
|
||||
): [string[], Date[]] => {
|
||||
@@ -54,7 +53,7 @@ export const parseRecurringDates = (
|
||||
});
|
||||
const dateStrings = times.map((t) => {
|
||||
// finally; show in local timeZone again
|
||||
return processDate(t.tz(timeZone), i18n, withDefaultTimeFormat);
|
||||
return processDate(t.tz(timeZone), i18n, selectedTimeFormat);
|
||||
});
|
||||
|
||||
return [dateStrings, times.map((t) => t.toDate())];
|
||||
|
||||
@@ -99,6 +99,14 @@ const nextConfig = {
|
||||
skipDefaultConversion: 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
|
||||
// "@calcom/ui": {
|
||||
// transform: "@calcom/ui/components/{{member}}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.7.0",
|
||||
"version": "2.7.8",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -11,6 +11,7 @@
|
||||
"dx": "yarn dev",
|
||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
@@ -60,10 +61,11 @@
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tanstack/react-query": "^4.3.9",
|
||||
"@tremor/react": "^2.0.0",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@vercel/edge-config": "^0.1.1",
|
||||
"@vercel/edge-functions-ui": "^0.2.1",
|
||||
"@vercel/og": "^0.0.21",
|
||||
"@vercel/og": "^0.5.0",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"async": "^3.2.4",
|
||||
"bcryptjs": "^2.4.3",
|
||||
@@ -111,13 +113,14 @@
|
||||
"react-multi-email": "^0.5.3",
|
||||
"react-phone-number-input": "^3.2.7",
|
||||
"react-schemaorg": "^2.0.0",
|
||||
"react-select": "^5.4.0",
|
||||
"react-timezone-select": "^1.3.2",
|
||||
"react-select": "^5.7.0",
|
||||
"react-timezone-select": "^1.4.0",
|
||||
"react-use-intercom": "1.5.1",
|
||||
"react-virtualized-auto-sizer": "^1.0.6",
|
||||
"react-window": "^1.8.7",
|
||||
"remark": "^14.0.2",
|
||||
"rrule": "^2.7.1",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"schema-dts": "^1.1.0",
|
||||
"short-uuid": "^4.2.0",
|
||||
"strip-markdown": "^5.0.0",
|
||||
@@ -155,6 +158,7 @@
|
||||
"@types/react-phone-number-input": "^3.0.14",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"@types/uuid": "8.3.1",
|
||||
"autoprefixer": "^10.4.12",
|
||||
|
||||
+42
-5
@@ -3,10 +3,10 @@ import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
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 { 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";
|
||||
|
||||
@@ -40,6 +40,43 @@ export default function Custom404() {
|
||||
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
||||
const isSignup = router.asPath.startsWith("/signup");
|
||||
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 (
|
||||
<>
|
||||
@@ -62,7 +99,7 @@ export default function Custom404() {
|
||||
<h1 className="font-cal mt-2 text-3xl font-extrabold text-gray-900">
|
||||
{t("signup_requires")}
|
||||
</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 className="mt-12">
|
||||
<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">
|
||||
<li className="px-4 py-2">
|
||||
<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">
|
||||
<div className="flex-shrink-0">
|
||||
<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>
|
||||
<div className="mt-8">
|
||||
<Link
|
||||
href={`${WEBSITE_URL}/enterprise`}
|
||||
href="https://cal.com/sales"
|
||||
className="text-base font-medium text-black hover:text-gray-500">
|
||||
{t("contact_sales")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
|
||||
@@ -24,7 +24,7 @@ import defaultEvents, {
|
||||
} from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 prisma from "@calcom/prisma";
|
||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||
@@ -147,7 +147,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<>
|
||||
<div
|
||||
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) => ({
|
||||
...eventType,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
}));
|
||||
|
||||
const isSingleUser = users.length === 1;
|
||||
@@ -352,9 +353,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
})
|
||||
: [];
|
||||
|
||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||
|
||||
return {
|
||||
props: {
|
||||
users,
|
||||
safeBio,
|
||||
profile,
|
||||
user: {
|
||||
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 hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
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 { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
@@ -59,7 +59,6 @@ Type.isThemeSupported = true;
|
||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
// 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 { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
||||
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 };
|
||||
|
||||
const [eventType]: ((typeof user.eventTypes)[number] & {
|
||||
@@ -153,7 +149,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
||||
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
|
||||
const hasActiveTeam =
|
||||
|
||||
@@ -13,7 +13,10 @@ import {
|
||||
getGroupName,
|
||||
getUsernameList,
|
||||
} 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 { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
||||
import {
|
||||
customInputSchema,
|
||||
@@ -21,8 +24,6 @@ import {
|
||||
userMetadata as userMetadataSchema,
|
||||
} from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { GetBookingType } from "@lib/getBooking";
|
||||
import getBooking from "@lib/getBooking";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import BookingPage from "@components/booking/pages/BookingPage";
|
||||
@@ -189,6 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
slug: u.username,
|
||||
theme: u.theme,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
};
|
||||
})[0];
|
||||
|
||||
@@ -217,7 +219,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
rescheduleUid = bookingSeat.booking.uid;
|
||||
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
|
||||
// the bookingUid regardless and use it to get the booking
|
||||
const currentSeats = await prisma.booking.findFirst({
|
||||
@@ -237,11 +241,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
|
||||
booking = await getBooking(
|
||||
prisma,
|
||||
rescheduleUid || query.bookingUid || bookingUidWithSeats || "",
|
||||
eventTypeObject.bookingFields
|
||||
);
|
||||
booking = await getBooking(prisma, rescheduleUid || query.bookingUid || bookingUidWithSeats || "");
|
||||
}
|
||||
|
||||
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
|
||||
@@ -285,12 +285,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
: eventType.recurringEvent.count)) ||
|
||||
null;
|
||||
|
||||
const currentSlotBooking = await getBooking(prisma, bookingUidWithSeats || "");
|
||||
|
||||
return {
|
||||
props: {
|
||||
away: user.away,
|
||||
profile,
|
||||
eventType: eventTypeObject,
|
||||
booking,
|
||||
currentSlotBooking: currentSlotBooking,
|
||||
recurringEventCount,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isDynamicGroupBooking,
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
import type { ResetPasswordRequest } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendPasswordResetEmail } from "@calcom/emails";
|
||||
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
|
||||
import rateLimit from "@calcom/lib/rateLimit";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const limiter = rateLimit({
|
||||
intervalInMs: 60 * 1000, // 1 minute
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const t = await getTranslation(req.body.language ?? "en", "common");
|
||||
const email = z
|
||||
.string()
|
||||
.email()
|
||||
.transform((val) => val.toLowerCase())
|
||||
.parse(req.body?.email);
|
||||
|
||||
const { isRateLimited } = limiter.check(10, email); // 10 requests per minute
|
||||
|
||||
if (isRateLimited) {
|
||||
return res.status(429).json({ message: "Too Many Requests." });
|
||||
}
|
||||
|
||||
if (req.method !== "POST") {
|
||||
return res.status(405).end();
|
||||
@@ -17,7 +34,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
try {
|
||||
const maybeUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
email: req.body?.email?.toLowerCase(),
|
||||
email,
|
||||
},
|
||||
select: {
|
||||
name: true,
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
|
||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
@@ -48,8 +49,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
eventType: {
|
||||
select: {
|
||||
recurringEvent: true,
|
||||
bookingFields: true,
|
||||
},
|
||||
},
|
||||
responses: true,
|
||||
uid: true,
|
||||
destinationCalendar: true,
|
||||
},
|
||||
@@ -96,6 +99,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
title: booking.title,
|
||||
description: booking.description || undefined,
|
||||
customInputs: isPrismaObjOrUndefined(booking.customInputs),
|
||||
...getCalEventResponses({
|
||||
bookingFields: booking.eventType?.bookingFields ?? null,
|
||||
booking,
|
||||
}),
|
||||
location: booking.location ?? "",
|
||||
startTime: booking.startTime.toISOString(),
|
||||
endTime: booking.endTime.toISOString(),
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
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) {
|
||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||
@@ -11,10 +19,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* Remove this endpoint
|
||||
*/
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ImageResponse } from "@vercel/og";
|
||||
import type { NextApiRequest } from "next";
|
||||
import { remark } from "remark";
|
||||
import type { SatoriOptions } from "satori";
|
||||
import strip from "strip-markdown";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
|
||||
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
|
||||
res.arrayBuffer()
|
||||
@@ -75,10 +74,13 @@ export default async function handler(req: NextApiRequest) {
|
||||
meetingImage: searchParams.get("meetingImage"),
|
||||
imageType,
|
||||
});
|
||||
|
||||
const title_ = md.render(title).replace(/(<([^>]+)>)/gi, "");
|
||||
|
||||
const img = new ImageResponse(
|
||||
(
|
||||
<Meeting
|
||||
title={title}
|
||||
title={title_}
|
||||
profile={{ name: meetingProfileName, image: meetingImage }}
|
||||
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
||||
/>
|
||||
@@ -86,7 +88,7 @@ export default async function handler(req: NextApiRequest) {
|
||||
ogConfig
|
||||
) as { body: Buffer };
|
||||
|
||||
return new Response(img.body, { status: 200 });
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
}
|
||||
case "app": {
|
||||
const { name, description, slug } = appSchema.parse({
|
||||
@@ -99,7 +101,7 @@ export default async function handler(req: NextApiRequest) {
|
||||
body: Buffer;
|
||||
};
|
||||
|
||||
return new Response(img.body, { status: 200 });
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
}
|
||||
|
||||
case "generic": {
|
||||
@@ -108,12 +110,12 @@ export default async function handler(req: NextApiRequest) {
|
||||
description: searchParams.get("description"),
|
||||
imageType,
|
||||
});
|
||||
const description_ = await (await remark().use(strip).process(description)).toString();
|
||||
const img = new ImageResponse(<Generic title={title} description={description_} />, ogConfig) as {
|
||||
|
||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
|
||||
body: Buffer;
|
||||
};
|
||||
|
||||
return new Response(img.body, { status: 200 });
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
}
|
||||
|
||||
default:
|
||||
|
||||
@@ -3,6 +3,7 @@ import matter from "gray-matter";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import type { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||
import path from "path";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -13,6 +14,23 @@ import App from "@components/apps/App";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||
|
||||
const sourceSchema = z.object({
|
||||
content: z.string(),
|
||||
data: z.object({
|
||||
description: z.string().optional(),
|
||||
items: z
|
||||
.array(
|
||||
z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
iframe: z.object({ src: z.string() }),
|
||||
}),
|
||||
])
|
||||
)
|
||||
.optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
return (
|
||||
<App
|
||||
@@ -33,7 +51,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
email={data.email}
|
||||
licenseRequired={data.licenseRequired}
|
||||
isProOnly={data.isProOnly}
|
||||
images={source.data?.items as string[] | undefined}
|
||||
descriptionItems={source.data?.items as string[] | undefined}
|
||||
isTemplate={data.isTemplate}
|
||||
dependencies={data.dependencies}
|
||||
// tos="https://zoom.us/terms"
|
||||
@@ -85,10 +103,11 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
source = singleApp.description;
|
||||
}
|
||||
|
||||
const { content, data } = matter(source);
|
||||
const result = matter(source);
|
||||
const { content, data } = sourceSchema.parse({ content: result.content, data: result.data });
|
||||
if (data.items) {
|
||||
data.items = data.items.map((item: string) => {
|
||||
if (!item.includes("/api/app-store")) {
|
||||
data.items = data.items.map((item) => {
|
||||
if (typeof item === "string" && !item.includes("/api/app-store")) {
|
||||
// Make relative paths absolute
|
||||
return `/api/app-store/${appDirname}/${item}`;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { FiSearch } from "@calcom/ui/components/icon";
|
||||
|
||||
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||
|
||||
import { ssgInit } from "@server/lib/ssg";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const tabs: HorizontalTabItemProps[] = [
|
||||
{
|
||||
@@ -88,7 +88,7 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
const ssg = await ssgInit(context);
|
||||
const ssr = await ssrInit(context);
|
||||
|
||||
const session = await getServerSession({ req, res });
|
||||
|
||||
@@ -119,7 +119,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return b.count - a.count;
|
||||
}),
|
||||
appStore,
|
||||
trpcState: ssg.dehydrate(),
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ResetPasswordRequest } from "@prisma/client";
|
||||
import debounce from "lodash/debounce";
|
||||
import { debounce } from "lodash";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
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 { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
@@ -43,7 +43,7 @@ export default function Logout(props: Props) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button href="/auth/login" passHref className="flex w-full justify-center">
|
||||
<Button href="/auth/login" className="flex w-full justify-center">
|
||||
{t("go_back_login")}
|
||||
</Button>
|
||||
</AuthContainer>
|
||||
|
||||
@@ -94,6 +94,7 @@ export default function Availability() {
|
||||
data: { schedule: scheduleId },
|
||||
} = useTypedQuery(querySchema);
|
||||
|
||||
const { fromEventType } = router.query;
|
||||
const { timeFormat } = me.data || { timeFormat: null };
|
||||
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId },
|
||||
@@ -153,7 +154,7 @@ export default function Availability() {
|
||||
|
||||
return (
|
||||
<Shell
|
||||
backPath="/availability"
|
||||
backPath={fromEventType ? true : "/availability"}
|
||||
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")}
|
||||
heading={
|
||||
<Controller
|
||||
@@ -200,7 +201,14 @@ export default function Availability() {
|
||||
<VerticalDivider />
|
||||
<Dialog>
|
||||
<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>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={deleteMutation.isLoading}
|
||||
@@ -223,7 +231,7 @@ export default function Availability() {
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<div className="w-full">
|
||||
<div className="mt-4 w-full md:mt-0">
|
||||
<Form
|
||||
form={form}
|
||||
id="availability-form"
|
||||
|
||||
@@ -93,7 +93,13 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
key={schedule.id}
|
||||
schedule={schedule}
|
||||
updateDefault={updateMutation.mutate}
|
||||
deleteFunction={deleteMutation.mutate}
|
||||
deleteFunction={() => {
|
||||
if (schedules.length === 1) {
|
||||
showToast(t("requires_at_least_one_schedule"), "error");
|
||||
} else {
|
||||
deleteMutation.mutate;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -6,7 +6,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { RRule } from "rrule";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
formatToLocalizedTimezone,
|
||||
} from "@calcom/lib/date-fns";
|
||||
import { getDefaultEvent } from "@calcom/lib/defaultEvents";
|
||||
import { getBookingWithResponses } from "@calcom/lib/getBooking";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
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 { timeZone } from "@lib/clock";
|
||||
import { getBookingWithResponses } from "@lib/getBooking";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import CancelBooking from "@components/booking/CancelBooking";
|
||||
@@ -59,114 +59,6 @@ import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
||||
|
||||
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>;
|
||||
|
||||
const stringToBoolean = z
|
||||
@@ -176,13 +68,14 @@ const stringToBoolean = z
|
||||
|
||||
const querySchema = z.object({
|
||||
uid: z.string(),
|
||||
allRemainingBookings: stringToBoolean,
|
||||
email: z.string().optional(),
|
||||
eventTypeSlug: z.string().optional(),
|
||||
cancel: stringToBoolean,
|
||||
allRemainingBookings: stringToBoolean,
|
||||
changes: stringToBoolean,
|
||||
reschedule: stringToBoolean,
|
||||
isSuccessBookingPage: z.string().optional(),
|
||||
isSuccessBookingPage: stringToBoolean,
|
||||
formerTime: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -266,16 +159,18 @@ export default function Success(props: SuccessProps) {
|
||||
const giphyImage = giphyAppData?.thankYouPage;
|
||||
|
||||
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 isCancelled =
|
||||
status === "CANCELLED" ||
|
||||
status === "REJECTED" ||
|
||||
(isCancellationMode &&
|
||||
(!!seatReferenceUid
|
||||
? !bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid)
|
||||
: !userIsOwner));
|
||||
(!!seatReferenceUid &&
|
||||
!bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid));
|
||||
|
||||
const telemetry = useTelemetry();
|
||||
useEffect(() => {
|
||||
@@ -402,9 +297,6 @@ export default function Success(props: SuccessProps) {
|
||||
<CustomBranding lightVal={props.profile.brandColor} darkVal={props.profile.darkBrandColor} />
|
||||
<main className={classNames(shouldAlignCentrally ? "mx-auto" : "", isEmbed ? "" : "max-w-3xl")}>
|
||||
<div className={classNames("overflow-y-auto", isEmbed ? "" : "z-50 ")}>
|
||||
{isSuccessBookingPage && !isCancellationMode && eventType.successRedirectUrl ? (
|
||||
<RedirectionToast url={eventType.successRedirectUrl} />
|
||||
) : null}{" "}
|
||||
<div
|
||||
className={classNames(
|
||||
shouldAlignCentrally ? "text-center" : "",
|
||||
@@ -433,7 +325,7 @@ export default function Success(props: SuccessProps) {
|
||||
: "",
|
||||
isCancelled ? "h-12 w-12 rounded-full bg-red-100" : ""
|
||||
)}>
|
||||
{giphyImage && !needsConfirmation && (
|
||||
{giphyImage && !needsConfirmation && !isCancelled && (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img src={giphyImage} alt="Gif from Giphy" />
|
||||
)}
|
||||
@@ -487,7 +379,7 @@ export default function Success(props: SuccessProps) {
|
||||
</>
|
||||
)}
|
||||
<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="col-span-2 mb-6 last:mb-0">
|
||||
{reschedule && !!formerTime && (
|
||||
@@ -529,9 +421,11 @@ export default function Success(props: SuccessProps) {
|
||||
</div>
|
||||
)}
|
||||
{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>}
|
||||
<p className="text-bookinglight">{attendee.email}</p>
|
||||
<p data-testid={`attendee-${attendee.email}`} className="text-bookinglight">
|
||||
{attendee.email}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -565,7 +459,8 @@ export default function Success(props: SuccessProps) {
|
||||
</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]) => {
|
||||
const field = eventType.bookingFields.find((field) => field.name === name);
|
||||
// We show location in the "where" section
|
||||
@@ -580,15 +475,10 @@ export default function Success(props: SuccessProps) {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mt-9 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}>
|
||||
{response.toString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="mt-4 font-medium">{label}</div>
|
||||
<p className="break-words" data-testid="field-response" data-fob-field={field.name}>
|
||||
{response.toString()}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
@@ -606,7 +496,7 @@ export default function Success(props: SuccessProps) {
|
||||
|
||||
{!props.recurringBookings && (
|
||||
<span className="text-bookinglight inline text-gray-700">
|
||||
<span className="underline">
|
||||
<span className="underline" data-testid="reschedule-link">
|
||||
<Link
|
||||
href={`/reschedule/${seatReferenceUid || bookingInfo?.uid}`}
|
||||
legacyBehavior>
|
||||
@@ -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 = (
|
||||
eventType: {
|
||||
seatsPerTimeSlot?: number | null;
|
||||
@@ -1017,9 +900,9 @@ const handleSeatsEventTypeOnBooking = (
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const ssr = await ssrInit(context);
|
||||
const parsedQuery = schema.safeParse(context.query);
|
||||
const parsedQuery = querySchema.safeParse(context.query);
|
||||
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({
|
||||
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]
|
||||
// probably fixable with json.stringify -> json.parse
|
||||
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),
|
||||
};
|
||||
|
||||
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl && !cancel) {
|
||||
if (eventType.successRedirectUrl && isSuccessBookingPage && !cancel) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: eventType.successRedirectUrl,
|
||||
|
||||
@@ -5,11 +5,12 @@ import type { LocationObject } from "@calcom/core/location";
|
||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
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 prisma from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import type { GetBookingType } from "@lib/getBooking";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
@@ -119,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
hideBranding: u.hideBranding,
|
||||
timeZone: u.timeZone,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(hashedLink.eventType.description),
|
||||
});
|
||||
|
||||
const [user] = users;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
||||
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -93,6 +94,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
brandColor: u.brandColor,
|
||||
darkBrandColor: u.darkBrandColor,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
};
|
||||
})[0];
|
||||
|
||||
@@ -120,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
profile,
|
||||
eventType: eventTypeObject,
|
||||
booking: null,
|
||||
currentSlotBooking: null,
|
||||
trpcState: ssr.dehydrate(),
|
||||
recurringEventCount,
|
||||
isDynamicGroupBooking: false,
|
||||
|
||||
@@ -13,11 +13,9 @@ import type { EventLocationType } from "@calcom/core/location";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import getEventTypeById from "@calcom/lib/getEventTypeById";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -65,6 +63,7 @@ export type FormValues = {
|
||||
hostPhoneNumber?: string;
|
||||
displayLocationPublicly?: boolean;
|
||||
phone?: string;
|
||||
hostDefault?: string;
|
||||
}[];
|
||||
customInputs: CustomInputParsed[];
|
||||
schedule: number | null;
|
||||
@@ -365,9 +364,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
};
|
||||
|
||||
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
|
||||
if (isLoading || !data) return null;
|
||||
return <EventTypePage {...data} />;
|
||||
const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
|
||||
|
||||
return <EventTypePage {...(data as EventTypeSetupProps)} />;
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
@@ -393,26 +392,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
|
||||
|
||||
return {
|
||||
props: {
|
||||
// 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;
|
||||
}
|
||||
await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
|
||||
return {
|
||||
props: {
|
||||
type: typeParam,
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default EventTypePageWrapper;
|
||||
|
||||
@@ -141,7 +141,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
||||
</div>
|
||||
<EventTypeDescription
|
||||
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||
eventType={type}
|
||||
eventType={{ ...type, descriptionAsSafeHTML: type.safeDescription }}
|
||||
shortenDescription
|
||||
/>
|
||||
</Link>
|
||||
@@ -351,11 +351,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.hidden && (
|
||||
<Badge variant="gray" size="lg">
|
||||
{t("hidden")}
|
||||
</Badge>
|
||||
)}
|
||||
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
|
||||
<Tooltip content={t("show_eventtype_on_profile")}>
|
||||
<div className="self-center rounded-md p-2 hover:bg-gray-200">
|
||||
<Switch
|
||||
|
||||
@@ -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,7 +1,6 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { HelpScout, useChat } from "react-live-chat-loader";
|
||||
|
||||
import { useIntercom } from "@calcom/features/ee/support/lib/intercom/useIntercom";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
@@ -33,15 +32,13 @@ const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
|
||||
|
||||
const BillingView = () => {
|
||||
const { t } = useLocale();
|
||||
const [, loadChat] = useChat();
|
||||
const [showChat, setShowChat] = useState(false);
|
||||
const { open } = useIntercom();
|
||||
const router = useRouter();
|
||||
const returnTo = router.asPath;
|
||||
const billingHref = `/api/integrations/stripepayment/portal?returnTo=${WEBAPP_URL}${returnTo}`;
|
||||
|
||||
const onContactSupportClick = () => {
|
||||
setShowChat(true);
|
||||
loadChat({ open: true });
|
||||
open();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -61,7 +58,6 @@ const BillingView = () => {
|
||||
{t("contact_support")}
|
||||
</Button>
|
||||
</CtaRow>
|
||||
{showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import crypto from "crypto";
|
||||
import { signOut } from "next-auth/react";
|
||||
import type { BaseSyntheticEvent } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
import { ErrorCode } from "@calcom/features/auth/lib/ErrorCode";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
@@ -315,13 +317,23 @@ const ProfileForm = ({
|
||||
extraField?: React.ReactNode;
|
||||
}) => {
|
||||
const { t } = useLocale();
|
||||
const emailMd5 = crypto
|
||||
.createHash("md5")
|
||||
.update(defaultValues.email || "example@example.com")
|
||||
.digest("hex");
|
||||
|
||||
const profileFormSchema = z.object({
|
||||
username: z.string(),
|
||||
avatar: z.string(),
|
||||
name: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(FULL_NAME_LENGTH_MAX_LIMIT, {
|
||||
message: t("max_limit_allowed_hint", { limit: FULL_NAME_LENGTH_MAX_LIMIT }),
|
||||
}),
|
||||
email: z.string().email(),
|
||||
bio: z.string(),
|
||||
});
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(profileFormSchema),
|
||||
});
|
||||
|
||||
const {
|
||||
@@ -338,7 +350,7 @@ const ProfileForm = ({
|
||||
name="avatar"
|
||||
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">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
|
||||
@@ -21,7 +21,8 @@ const PasswordView = () => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const metadata = userMetadata.parse(user?.metadata);
|
||||
const metadata = userMetadata.safeParse(user?.metadata);
|
||||
const sessionTimeout = metadata.success ? metadata.data?.sessionTimeout : undefined;
|
||||
|
||||
const sessionMutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: () => {
|
||||
@@ -36,10 +37,10 @@ const PasswordView = () => {
|
||||
const previousValue = utils.viewer.me.getData();
|
||||
const previousMetadata = userMetadata.parse(previousValue?.metadata);
|
||||
|
||||
if (previousValue && metadata?.sessionTimeout) {
|
||||
if (previousValue && sessionTimeout) {
|
||||
utils.viewer.me.setData(undefined, {
|
||||
...previousValue,
|
||||
metadata: { ...previousMetadata, sessionTimeout: metadata?.sessionTimeout },
|
||||
metadata: { ...previousMetadata, sessionTimeout: sessionTimeout },
|
||||
});
|
||||
}
|
||||
return { previousValue };
|
||||
@@ -81,19 +82,19 @@ const PasswordView = () => {
|
||||
defaultValues: {
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
sessionTimeout: metadata?.sessionTimeout,
|
||||
sessionTimeout,
|
||||
},
|
||||
});
|
||||
|
||||
const sessionTimeoutWatch = formMethods.watch("sessionTimeout");
|
||||
|
||||
const handleSubmit = (values: ChangePasswordSessionFormValues) => {
|
||||
const { oldPassword, newPassword, sessionTimeout } = values;
|
||||
const { oldPassword, newPassword, sessionTimeout: newSessionTimeout } = values;
|
||||
if (oldPassword && newPassword) {
|
||||
passwordMutation.mutate({ oldPassword, newPassword });
|
||||
}
|
||||
if (metadata?.sessionTimeout !== sessionTimeout) {
|
||||
sessionMutation.mutate({ metadata: { ...metadata, sessionTimeout } });
|
||||
if (sessionTimeout !== newSessionTimeout) {
|
||||
sessionMutation.mutate({ metadata: { ...metadata, sessionTimeout: newSessionTimeout } });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -106,6 +107,7 @@ const PasswordView = () => {
|
||||
|
||||
const passwordMinLength = data?.user.role === "USER" ? 7 : 15;
|
||||
const isUser = data?.user.role === "USER";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("password")} description={t("password_description")} />
|
||||
@@ -176,8 +178,8 @@ const PasswordView = () => {
|
||||
<Select
|
||||
options={timeoutOptions}
|
||||
defaultValue={
|
||||
metadata?.sessionTimeout
|
||||
? timeoutOptions.find((tmo) => tmo.value === metadata.sessionTimeout)
|
||||
sessionTimeout
|
||||
? timeoutOptions.find((tmo) => tmo.value === sessionTimeout)
|
||||
: timeoutOptions[1]
|
||||
}
|
||||
isSearchable={false}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useState } from "react";
|
||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
||||
@@ -32,11 +32,14 @@ const TwoFactorAuthView = () => {
|
||||
|
||||
if (isLoading) return <SkeletonLoader />;
|
||||
|
||||
const isCalProvider = user?.identityProvider === "CAL";
|
||||
return (
|
||||
<>
|
||||
<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">
|
||||
<Switch
|
||||
disabled={!isCalProvider}
|
||||
checked={user?.twoFactorEnabled}
|
||||
onCheckedChange={() =>
|
||||
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
|
||||
|
||||
@@ -19,7 +19,7 @@ const OnboardTeamMembersPage = () => {
|
||||
};
|
||||
|
||||
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
|
||||
<WizardLayout currentStep={1} maxSteps={2}>
|
||||
<WizardLayout currentStep={2} maxSteps={2}>
|
||||
{page}
|
||||
</WizardLayout>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import Head from "next/head";
|
||||
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { getLayout } from "@components/layouts/WizardLayout";
|
||||
import WizardLayout from "@components/layouts/WizardLayout";
|
||||
|
||||
const CreateNewTeamPage = () => {
|
||||
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;
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -113,7 +113,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
<>
|
||||
<div
|
||||
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,
|
||||
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 {
|
||||
props: {
|
||||
team,
|
||||
team: { ...team, safeBio, members },
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -2,15 +2,15 @@ import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import type { LocationObject } from "@calcom/core/location";
|
||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
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 { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
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 { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
@@ -64,7 +64,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
hideBranding: true,
|
||||
@@ -171,13 +170,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
hideBranding,
|
||||
timeZone,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
});
|
||||
|
||||
eventTypeObject.availability = [];
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBooking(prisma, rescheduleUid, getBookingFieldsWithSystemFields(eventTypeObject));
|
||||
booking = await getBooking(prisma, rescheduleUid);
|
||||
}
|
||||
|
||||
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 { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
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 { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
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 BookingPage from "@components/booking/pages/BookingPage";
|
||||
@@ -121,13 +122,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
image: u.avatar,
|
||||
slug: u.username,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
};
|
||||
})[0];
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
|
||||
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
|
||||
@@ -152,6 +154,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
eventType: eventTypeObject,
|
||||
recurringEventCount,
|
||||
booking,
|
||||
currentSlotBooking: null,
|
||||
isDynamicGroupBooking: false,
|
||||
hasHashedBookingLink: false,
|
||||
hashedLink: null,
|
||||
|
||||
@@ -93,6 +93,7 @@ interface ProgressBarProps {
|
||||
}
|
||||
|
||||
function ProgressBar(props: ProgressBarProps) {
|
||||
const { t } = useLocale();
|
||||
const { startTime, endTime } = props;
|
||||
const currentTime = dayjs().second(0).millisecond(0);
|
||||
const startingTime = dayjs(startTime).second(0).millisecond(0);
|
||||
@@ -111,7 +112,9 @@ function ProgressBar(props: ProgressBarProps) {
|
||||
const percentage = prev * (100 / startDuration);
|
||||
return (
|
||||
<div>
|
||||
<p>{duration} minutes</p>
|
||||
<p>
|
||||
{duration} {t("minutes")}
|
||||
</p>
|
||||
<div className="relative h-2 max-w-xl overflow-hidden rounded-full">
|
||||
<div className="absolute h-full w-full bg-gray-500/10" />
|
||||
<div className={classNames("relative h-full bg-green-500")} style={{ width: `${percentage}%` }} />
|
||||
@@ -127,6 +130,7 @@ interface VideoMeetingInfo {
|
||||
export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const { booking } = props;
|
||||
const { t } = useLocale();
|
||||
|
||||
const endTime = new Date(booking.endTime);
|
||||
const startTime = new Date(booking.startTime);
|
||||
@@ -139,25 +143,25 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
||||
open ? "translate-x-0" : "-translate-x-[232px]"
|
||||
)}>
|
||||
<main className="prose-sm prose max-w-64 prose-a:text-white prose-h3:text-white prose-h3:font-cal scroll-bar scrollbar-track-w-20 w-full overflow-scroll overflow-x-hidden border-r border-gray-300/20 bg-black/80 p-4 text-white shadow-sm backdrop-blur-lg">
|
||||
<h3>What:</h3>
|
||||
<h3>{t("what")}:</h3>
|
||||
<p>{booking.title}</p>
|
||||
<h3>Invitee Time Zone:</h3>
|
||||
<h3>{t("invitee_timezone")}:</h3>
|
||||
<p>{booking.user?.timeZone}</p>
|
||||
<h3>When:</h3>
|
||||
<h3>{t("when")}:</h3>
|
||||
<p>
|
||||
{formatToLocalizedDate(startTime)} <br />
|
||||
{formatToLocalizedTime(startTime)}
|
||||
</p>
|
||||
<h3>Time left</h3>
|
||||
<h3>{t("time_left")}</h3>
|
||||
<ProgressBar
|
||||
key={String(open)}
|
||||
endTime={endTime.toISOString()}
|
||||
startTime={startTime.toISOString()}
|
||||
/>
|
||||
|
||||
<h3>Who:</h3>
|
||||
<h3>{t("who")}:</h3>
|
||||
<p>
|
||||
{booking?.user?.name} - Organizer{" "}
|
||||
{booking?.user?.name} - {t("organizer")}:{" "}
|
||||
<a href={`mailto:${booking?.user?.email}`}>{booking?.user?.email}</a>
|
||||
</p>
|
||||
|
||||
@@ -169,12 +173,16 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
||||
))
|
||||
: null}
|
||||
|
||||
<h3>Description</h3>
|
||||
{booking.description && (
|
||||
<>
|
||||
<h3>{t("description")}:</h3>
|
||||
|
||||
<div
|
||||
className="prose-sm prose prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(booking.description ?? "") }}
|
||||
/>
|
||||
<div
|
||||
className="prose-sm prose prose-invert"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(booking.description ?? "") }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
|
||||
@@ -132,4 +132,26 @@ test.describe("pro user", () => {
|
||||
// This is the only booking in there that needed confirmation and now it should be empty screen
|
||||
await expect(page.locator('[data-testid="empty-screen"]')).toBeVisible();
|
||||
});
|
||||
|
||||
test("can book with multiple guests", async ({ page, users }) => {
|
||||
const additionalGuests = ["test@gmail.com", "test2@gmail.com"];
|
||||
|
||||
await page.click('[data-testid="event-type-link"]');
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await page.fill('[name="name"]', "test1234");
|
||||
await page.fill('[name="email"]', "test1234@example.com");
|
||||
await page.locator('[data-testid="add-guests"]').click();
|
||||
|
||||
await page.locator('input[type="email"]').nth(1).fill(additionalGuests[0]);
|
||||
await page.locator('[data-testid="add-another-guest"]').click();
|
||||
await page.locator('input[type="email"]').nth(2).fill(additionalGuests[1]);
|
||||
|
||||
await page.locator('[data-testid="confirm-book-button"]').click();
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
additionalGuests.forEach(async (email) => {
|
||||
await expect(page.locator(`[data-testid="attendee-${email}"]`)).toHaveText(email);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -119,7 +119,7 @@ test.describe("Event Types tests", () => {
|
||||
await page.locator("#location-select").click();
|
||||
await page.locator("text=In Person (Organizer Address)").click();
|
||||
// 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("[data-testid=display-location]").check();
|
||||
await page.locator("[data-testid=update-location]").click();
|
||||
|
||||
@@ -45,7 +45,7 @@ export const createEmbedsFixture = (page: Page) => {
|
||||
}
|
||||
api("on", {
|
||||
action: "*",
|
||||
callback: (e: any) => {
|
||||
callback: (e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @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
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { createServer } from "http";
|
||||
import noop from "lodash/noop";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import { test } from "./fixtures";
|
||||
|
||||
|
||||
@@ -33,56 +33,108 @@ test.describe("Manage Booking Questions", () => {
|
||||
await firstEventTypeElement.click();
|
||||
});
|
||||
|
||||
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
||||
await addQuestionAndSave({
|
||||
page,
|
||||
question: {
|
||||
name: "how_are_you",
|
||||
type: "Name",
|
||||
label: "How are you?",
|
||||
placeholder: "I'm fine, thanks",
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
||||
});
|
||||
});
|
||||
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
||||
const userFieldLocator = allFieldsLocator.nth(5);
|
||||
test.describe("For Team EventType", () => {
|
||||
test("Do a booking with a user added question and verify a few thing in b/w", async ({
|
||||
page,
|
||||
users,
|
||||
context,
|
||||
}, testInfo) => {
|
||||
// Considering there are many steps in it, it would need more than default test timeout
|
||||
test.setTimeout(testInfo.timeout * 3);
|
||||
const user = await createAndLoginUserWithEventTypes({ users });
|
||||
|
||||
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
||||
// There are 2 labels right now. Will be one in future. The second one is hidden
|
||||
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
||||
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
|
||||
});
|
||||
const webhookReceiver = await addWebhook(user);
|
||||
|
||||
await test.step("Go to First Team Event", async () => {
|
||||
const $eventTypes = page.locator("[data-testid=event-types]").nth(1).locator("li a");
|
||||
const firstEventTypeElement = $eventTypes.first();
|
||||
|
||||
await firstEventTypeElement.click();
|
||||
});
|
||||
|
||||
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
||||
await toggleQuestionAndSave({
|
||||
name: "how_are_you",
|
||||
page,
|
||||
});
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||
await expect(formBuilderFieldLocator).toBeHidden();
|
||||
});
|
||||
});
|
||||
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Show Question Again", async () => {
|
||||
await toggleQuestionAndSave({
|
||||
name: "how_are_you",
|
||||
page,
|
||||
});
|
||||
});
|
||||
async function runTestStepsCommonForTeamAndUserEventType(
|
||||
page: Page,
|
||||
context: PlaywrightTestArgs["context"],
|
||||
webhookReceiver: {
|
||||
port: number;
|
||||
close: () => import("http").Server;
|
||||
requestList: (import("http").IncomingMessage & { body?: unknown })[];
|
||||
url: string;
|
||||
}
|
||||
) {
|
||||
await page.click('[href$="tabName=advanced"]');
|
||||
|
||||
await test.step('Try to book without providing "How are you?" response', async () => {
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
||||
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
||||
});
|
||||
});
|
||||
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
||||
await addQuestionAndSave({
|
||||
page,
|
||||
question: {
|
||||
name: "how_are_you",
|
||||
type: "Name",
|
||||
label: "How are you?",
|
||||
placeholder: "I'm fine, thanks",
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
await test.step("Do a booking", async () => {
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
||||
const userFieldLocator = allFieldsLocator.nth(5);
|
||||
|
||||
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
||||
// There are 2 labels right now. Will be one in future. The second one is hidden
|
||||
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
||||
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
||||
await toggleQuestionAndSave({
|
||||
name: "how_are_you",
|
||||
page,
|
||||
});
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||
await expect(formBuilderFieldLocator).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Show Question Again", async () => {
|
||||
await toggleQuestionAndSave({
|
||||
name: "how_are_you",
|
||||
page,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Try to book without providing "How are you?" response', async () => {
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
||||
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Make rescheduleReason required - It won't be required for a fresh booking", async () => {
|
||||
await toggleQuestionRequireStatusAndSave({
|
||||
required: true,
|
||||
name: "rescheduleReason",
|
||||
page,
|
||||
});
|
||||
});
|
||||
|
||||
const previewTabPage =
|
||||
await test.step("Do a booking and notice that we can book without giving a value for rescheduleReason", async () => {
|
||||
return await doOnFreshPreview(
|
||||
page,
|
||||
context,
|
||||
async (page) => {
|
||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||
await expect(formBuilderFieldLocator).toBeVisible();
|
||||
expect(
|
||||
@@ -133,146 +185,16 @@ test.describe("Manage Booking Questions", () => {
|
||||
value: "I am great!",
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("For Team EventType", () => {
|
||||
test("Do a booking with a user added question and verify a few thing in b/w", async ({
|
||||
page,
|
||||
users,
|
||||
context,
|
||||
}, testInfo) => {
|
||||
// Considering there are many steps in it, it would need more than default test timeout
|
||||
test.setTimeout(testInfo.timeout * 3);
|
||||
const user = await createAndLoginUserWithEventTypes({ users });
|
||||
|
||||
const webhookReceiver = await addWebhook(user);
|
||||
|
||||
await test.step("Go to First Team Event", async () => {
|
||||
const $eventTypes = page.locator("[data-testid=event-types]").nth(1).locator("li a");
|
||||
const firstEventTypeElement = $eventTypes.first();
|
||||
|
||||
await firstEventTypeElement.click();
|
||||
});
|
||||
|
||||
await runTestStepsCommonForTeamAndUserEventType(page, context, webhookReceiver);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function runTestStepsCommonForTeamAndUserEventType(
|
||||
page: Page,
|
||||
context: PlaywrightTestArgs["context"],
|
||||
webhookReceiver: {
|
||||
port: number;
|
||||
close: () => import("http").Server;
|
||||
requestList: (import("http").IncomingMessage & { body?: unknown })[];
|
||||
url: string;
|
||||
}
|
||||
) {
|
||||
await test.step("Add Question and see that it's shown on Booking Page at appropriate position", async () => {
|
||||
await addQuestionAndSave({
|
||||
page,
|
||||
question: {
|
||||
name: "how_are_you",
|
||||
type: "Name",
|
||||
label: "How are you?",
|
||||
placeholder: "I'm fine, thanks",
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const allFieldsLocator = await expectSystemFieldsToBeThere(page);
|
||||
const userFieldLocator = allFieldsLocator.nth(5);
|
||||
|
||||
await expect(userFieldLocator.locator('[name="how_are_you"]')).toBeVisible();
|
||||
// There are 2 labels right now. Will be one in future. The second one is hidden
|
||||
expect(await userFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
||||
await expect(userFieldLocator.locator("input[type=text]")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Hide Question and see that it's not shown on Booking Page", async () => {
|
||||
await toggleQuestionAndSave({
|
||||
name: "how_are_you",
|
||||
page,
|
||||
});
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||
await expect(formBuilderFieldLocator).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Show Question Again", async () => {
|
||||
await toggleQuestionAndSave({
|
||||
name: "how_are_you",
|
||||
page,
|
||||
});
|
||||
});
|
||||
|
||||
await test.step('Try to book without providing "How are you?" response', async () => {
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
||||
await expectErrorToBeThereFor({ page, name: "how_are_you" });
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Do a booking", async () => {
|
||||
await doOnFreshPreview(page, context, async (page) => {
|
||||
const formBuilderFieldLocator = page.locator('[data-fob-field-name="how_are_you"]');
|
||||
await expect(formBuilderFieldLocator).toBeVisible();
|
||||
expect(await formBuilderFieldLocator.locator('[name="how_are_you"]').getAttribute("placeholder")).toBe(
|
||||
"I'm fine, thanks"
|
||||
},
|
||||
true
|
||||
);
|
||||
expect(await formBuilderFieldLocator.locator("label").nth(0).innerText()).toBe("How are you?");
|
||||
await formBuilderFieldLocator.locator('[name="how_are_you"]').fill("I am great!");
|
||||
await bookTimeSlot({ page, name: "Booker", email: "booker@example.com" });
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
expect(
|
||||
await page.locator('[data-testid="field-response"][data-fob-field="how_are_you"]').innerText()
|
||||
).toBe("I am great!");
|
||||
|
||||
await waitFor(() => {
|
||||
expect(webhookReceiver.requestList.length).toBe(1);
|
||||
});
|
||||
|
||||
const [request] = webhookReceiver.requestList;
|
||||
|
||||
const payload = (request.body as any).payload as any;
|
||||
|
||||
expect(payload.responses).toMatchObject({
|
||||
email: {
|
||||
label: "email_address",
|
||||
value: "booker@example.com",
|
||||
},
|
||||
how_are_you: {
|
||||
label: "How are you?",
|
||||
value: "I am great!",
|
||||
},
|
||||
name: {
|
||||
label: "your_name",
|
||||
value: "Booker",
|
||||
},
|
||||
});
|
||||
|
||||
expect(payload.location).toBe("integrations:daily");
|
||||
|
||||
expect(payload.attendees[0]).toMatchObject({
|
||||
name: "Booker",
|
||||
email: "booker@example.com",
|
||||
});
|
||||
|
||||
expect(payload.userFieldsResponses).toMatchObject({
|
||||
how_are_you: {
|
||||
label: "How are you?",
|
||||
value: "I am great!",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
await test.step("Do a reschedule and notice that we can't book without giving a value for rescheduleReason", async () => {
|
||||
const page = previewTabPage;
|
||||
await rescheduleFromTheLinkOnPage({ page });
|
||||
await page.pause();
|
||||
await expectErrorToBeThereFor({ page, name: "rescheduleReason" });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -333,7 +255,6 @@ async function addQuestionAndSave({
|
||||
required?: boolean;
|
||||
};
|
||||
}) {
|
||||
await page.click('[href$="tabName=advanced"]');
|
||||
await page.click('[data-testid="add-field"]');
|
||||
|
||||
if (question.type !== undefined) {
|
||||
@@ -379,11 +300,15 @@ async function expectErrorToBeThereFor({ page, name }: { page: Page; name: strin
|
||||
async function doOnFreshPreview(
|
||||
page: Page,
|
||||
context: PlaywrightTestArgs["context"],
|
||||
callback: (page: Page) => Promise<void>
|
||||
callback: (page: Page) => Promise<void>,
|
||||
persistTab = false
|
||||
) {
|
||||
const previewTabPage = await openBookingFormInPreviewTab(context, page);
|
||||
await callback(previewTabPage);
|
||||
await previewTabPage.close();
|
||||
if (!persistTab) {
|
||||
await previewTabPage.close();
|
||||
}
|
||||
return previewTabPage;
|
||||
}
|
||||
|
||||
async function toggleQuestionAndSave({ name, page }: { name: string; page: Page }) {
|
||||
@@ -391,6 +316,25 @@ async function toggleQuestionAndSave({ name, page }: { name: string; page: Page
|
||||
await saveEventType(page);
|
||||
}
|
||||
|
||||
async function toggleQuestionRequireStatusAndSave({
|
||||
required,
|
||||
name,
|
||||
page,
|
||||
}: {
|
||||
required: boolean;
|
||||
name: string;
|
||||
page: Page;
|
||||
}) {
|
||||
await page.locator(`[data-testid="field-${name}"]`).locator('[data-testid="edit-field-action"]').click();
|
||||
await page
|
||||
.locator('[data-testid="edit-field-dialog"]')
|
||||
.locator('[data-testid="field-required"] button')
|
||||
.locator(`text=${required ? "Yes" : "No"}`)
|
||||
.click();
|
||||
await page.locator('[data-testid="field-add-save"]').click();
|
||||
await saveEventType(page);
|
||||
}
|
||||
|
||||
async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<typeof createUsersFixture> }) {
|
||||
const user = await users.create(null, {
|
||||
hasTeam: true,
|
||||
@@ -399,6 +343,16 @@ async function createAndLoginUserWithEventTypes({ users }: { users: ReturnType<t
|
||||
return user;
|
||||
}
|
||||
|
||||
async function rescheduleFromTheLinkOnPage({ page }: { page: Page }) {
|
||||
await page.locator('[data-testid="reschedule-link"]').click();
|
||||
await page.waitForLoadState();
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
await page.waitForNavigation({
|
||||
url: (url) => url.pathname.endsWith("/book"),
|
||||
});
|
||||
await page.click('[data-testid="confirm-reschedule-button"]');
|
||||
}
|
||||
|
||||
async function openBookingFormInPreviewTab(context: PlaywrightTestArgs["context"], page: Page) {
|
||||
const previewTabPromise = context.waitForEvent("page");
|
||||
await page.locator('[data-testid="preview-button"]').click();
|
||||
|
||||
@@ -26,7 +26,11 @@ test.describe("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[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();
|
||||
|
||||
|
||||
@@ -97,12 +97,19 @@ test.describe("Reschedule Tests", async () => {
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
// NOTE: remove if old booking should not be deleted
|
||||
expect(await (await booking.self()).status).toBe(BookingStatus.CANCELLED);
|
||||
const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking.uid } });
|
||||
const rescheduledBooking = await prisma.booking.findFirstOrThrow({ where: { uid: booking.uid } });
|
||||
|
||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } });
|
||||
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 }) => {
|
||||
@@ -148,7 +155,6 @@ test.describe("Reschedule Tests", async () => {
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/.*payment/);
|
||||
await payment.delete();
|
||||
});
|
||||
|
||||
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 expect(page).toHaveURL(/.*booking/);
|
||||
|
||||
await payment.delete();
|
||||
});
|
||||
|
||||
test("Opt in event should be PENDING when rescheduled by USER", async ({ page, users, bookings }) => {
|
||||
|
||||
@@ -4,4 +4,6 @@ Disallow: /api
|
||||
Disallow: /static/locales
|
||||
|
||||
# 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/
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
@@ -90,6 +90,7 @@
|
||||
"new_event_scheduled_recurring": "A new recurring event has been scheduled.",
|
||||
"invitee_email": "Invitee Email",
|
||||
"invitee_timezone": "Invitee Time Zone",
|
||||
"time_left": "Time left",
|
||||
"event_type": "Event Type",
|
||||
"enter_meeting": "Enter Meeting",
|
||||
"video_call_provider": "Video call provider",
|
||||
@@ -110,7 +111,7 @@
|
||||
"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.",
|
||||
"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_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",
|
||||
@@ -403,6 +404,7 @@
|
||||
"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.",
|
||||
"2fa": "Two-Factor Authentication",
|
||||
"2fa_disabled": "Two-Factor authentication can only be enabled for email and password authentication",
|
||||
"enable_2fa": "Enable 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.",
|
||||
@@ -432,6 +434,7 @@
|
||||
"password_hint_min": "Minimum 7 characters long",
|
||||
"password_hint_admin_min": "Minimum 15 characters long",
|
||||
"password_hint_num": "Contain at least 1 number",
|
||||
"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",
|
||||
"incorrect_password": "Password is incorrect.",
|
||||
"incorrect_username_password": "Username or password is incorrect.",
|
||||
@@ -861,7 +864,7 @@
|
||||
"add_new_calendar": "Add a new calendar",
|
||||
"set_calendar": "Set where to add new events to when you're booked.",
|
||||
"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",
|
||||
"availability_updated_successfully": "{{scheduleName}} schedule updated successfully",
|
||||
"schedule_deleted_successfully": "Schedule deleted successfully",
|
||||
@@ -1216,6 +1219,7 @@
|
||||
"impersonation": "Impersonation",
|
||||
"impersonation_description": "Settings to manage user impersonation",
|
||||
"users": "Users",
|
||||
"user": "User",
|
||||
"profile_description": "Manage settings for your {{appName}} profile",
|
||||
"users_description": "Here you can find a list of all users",
|
||||
"users_listing": "User listing",
|
||||
@@ -1497,7 +1501,7 @@
|
||||
"date_overrides_update_btn": "Update Override",
|
||||
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
||||
"set_as_default": "Set as default",
|
||||
"hide_eventtype_details": "Hide EventType Details",
|
||||
"hide_eventtype_details": "Hide event type details",
|
||||
"show_navigation": "Show navigation",
|
||||
"hide_navigation": "Hide navigation",
|
||||
"verification_code_sent": "Verification code sent",
|
||||
@@ -1511,7 +1515,7 @@
|
||||
"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",
|
||||
"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",
|
||||
"username_already_taken": "Username is already taken",
|
||||
"assignment": "Assignment",
|
||||
@@ -1606,6 +1610,7 @@
|
||||
"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.",
|
||||
"change_default_conferencing_app": "Set as default",
|
||||
"organizer_default_conferencing_app": "Organizer's default app",
|
||||
"under_maintenance": "Down for maintenance",
|
||||
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
|
||||
"event_type_seats": "{{numberOfSeats}} seats",
|
||||
@@ -1629,22 +1634,30 @@
|
||||
"a_routing_form": "A Routing Form",
|
||||
"form_description_placeholder": "Form Description",
|
||||
"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",
|
||||
"delete_form": "Are you sure you want to delete this 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_2":"All associated responses will be deleted.",
|
||||
"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.",
|
||||
"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.",
|
||||
"connected_forms": "Connected Forms",
|
||||
"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.",
|
||||
"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.",
|
||||
"add_1_option_per_line": "Add 1 option per line",
|
||||
"select_a_router": "Select a router",
|
||||
"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",
|
||||
"this_will_be_the_placeholder": "This will be the placeholder",
|
||||
"this_meeting_has_not_started_yet": "This meeting has not started yet",
|
||||
@@ -1655,8 +1668,33 @@
|
||||
"verification_code": "Verification code",
|
||||
"can_you_try_again": "Can you try again with a different time?",
|
||||
"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",
|
||||
"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_description": "Configuración para administrar la suplantación del usuario",
|
||||
"users": "Usuarios",
|
||||
"user": "Usuario",
|
||||
"profile_description": "Administra los ajustes para tu perfil de {{appName}}",
|
||||
"users_description": "Aquí puede encontrar una lista de todos los usuarios",
|
||||
"users_listing": "Lista de usuarios",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
@@ -29,7 +29,6 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
|
||||
ctx: {
|
||||
prisma,
|
||||
session: null,
|
||||
user: null,
|
||||
locale,
|
||||
i18n: _i18n,
|
||||
},
|
||||
|
||||
@@ -148,8 +148,6 @@ select:focus {
|
||||
--tw-ring-color: var(--brand-color);
|
||||
border-color: var(--brand-color);
|
||||
}
|
||||
/* https://stackoverflow.com/a/69308190 */
|
||||
input[type='text']:focus { box-shadow: none; }
|
||||
|
||||
|
||||
@layer components {
|
||||
@@ -497,3 +495,5 @@ hr {
|
||||
::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@ const base = require("@calcom/config/tailwind-preset");
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
...base,
|
||||
content: [...base.content],
|
||||
content: [...base.content, "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
|
||||
};
|
||||
|
||||
@@ -112,4 +112,25 @@ describe("Tests the slot logic", () => {
|
||||
})
|
||||
).toHaveLength(11);
|
||||
});
|
||||
|
||||
it("shows correct time slots for 20 minutes long events with working hours that do not end at a full hour ", async () => {
|
||||
// 72 20-minutes events in a 24h day
|
||||
expect(
|
||||
getSlots({
|
||||
inviteeDate: dayjs.utc().add(1, "day"),
|
||||
frequency: 20,
|
||||
minimumBookingNotice: 0,
|
||||
workingHours: [
|
||||
{
|
||||
userId: 1,
|
||||
days: Array.from(Array(7).keys()),
|
||||
startTime: MINUTES_DAY_START,
|
||||
endTime: MINUTES_DAY_END - 14, // 23:45
|
||||
},
|
||||
],
|
||||
eventLength: 20,
|
||||
organizerTimeZone: "America/Toronto",
|
||||
})
|
||||
).toHaveLength(71);
|
||||
});
|
||||
});
|
||||
|
||||
+4
-2
@@ -70,6 +70,7 @@
|
||||
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
||||
"test": "jest",
|
||||
"type-check": "turbo run type-check",
|
||||
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
||||
"web": "yarn workspace @calcom/web"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -86,14 +87,15 @@
|
||||
"jest-watch-select-projects": "^2.0.0",
|
||||
"jest-watch-typeahead": "^2.0.0",
|
||||
"lint-staged": "^12.5.0",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier": "^2.8.6",
|
||||
"ts-jest": "^28.0.8",
|
||||
"tsc-absolute": "^1.0.0",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"city-timezones": "^1.2.1",
|
||||
"eslint": "^8.34.0",
|
||||
"turbo": "^1.4.3"
|
||||
"turbo": "^1.8.3"
|
||||
},
|
||||
"resolutions": {
|
||||
"@apidevtools/json-schema-ref-parser": "9.0.9",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "fs";
|
||||
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 execSync from "./utils/execSync";
|
||||
@@ -127,6 +127,8 @@ export const BaseAppFork = {
|
||||
.replace(/_DESCRIPTION_/g, description)
|
||||
.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 }) {
|
||||
|
||||
@@ -40,6 +40,7 @@ export const EventTypeAddonMap = {
|
||||
fathom: dynamic(() => import("./fathom/components/EventTypeAppCardInterface")),
|
||||
ga4: dynamic(() => import("./ga4/components/EventTypeAppCardInterface")),
|
||||
giphy: dynamic(() => import("./giphy/components/EventTypeAppCardInterface")),
|
||||
"google-tag-manager": dynamic(() => import("./google-tag-manager/components/EventTypeAppCardInterface")),
|
||||
plausible: dynamic(() => import("./plausible/components/EventTypeAppCardInterface")),
|
||||
qr_code: dynamic(() => import("./qr_code/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 ga4_zod_ts } from "./ga4/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 hubspot_zod_ts } from "./hubspot/zod";
|
||||
import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod";
|
||||
@@ -30,6 +31,7 @@ export const appKeysSchemas = {
|
||||
fathom: fathom_zod_ts,
|
||||
ga4: ga4_zod_ts,
|
||||
giphy: giphy_zod_ts,
|
||||
"google-tag-manager": google_tag_manager_zod_ts,
|
||||
googlecalendar: googlecalendar_zod_ts,
|
||||
hubspot: hubspot_zod_ts,
|
||||
larkcalendar: larkcalendar_zod_ts,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user