Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -12,13 +12,13 @@ Fixes # (issue)
|
|||||||
|
|
||||||
## Type of change
|
## Type of change
|
||||||
|
|
||||||
<!-- Please delete options that are not relevant. -->
|
<!-- Please delete bullets that are not relevant. -->
|
||||||
|
|
||||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
- Bug fix (non-breaking change which fixes an issue)
|
||||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
- Chore (refactoring code, technical debt, workflow improvements)
|
||||||
- [ ] New feature (non-breaking change which adds functionality)
|
- New feature (non-breaking change which adds functionality)
|
||||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||||
- [ ] This change requires a documentation update
|
- This change requires a documentation update
|
||||||
|
|
||||||
## How should this be tested?
|
## How should this be tested?
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"problemMatcher": [
|
||||||
|
{
|
||||||
|
"owner": "tsc-absolute",
|
||||||
|
"pattern": [
|
||||||
|
{
|
||||||
|
"regexp": "(?:^|\\s)([^\\s].*)[\\(:](\\d+)[,:](\\d+)(?:\\):\\s+|\\s+-\\s+)(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
|
||||||
|
"file": 1,
|
||||||
|
"line": 2,
|
||||||
|
"column": 3,
|
||||||
|
"severity": 4,
|
||||||
|
"code": 5,
|
||||||
|
"message": 6
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -10,8 +10,10 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: ./.github/actions/dangerous-git-checkout
|
- uses: ./.github/actions/dangerous-git-checkout
|
||||||
- uses: ./.github/actions/yarn-install
|
- uses: ./.github/actions/yarn-install
|
||||||
|
|
||||||
- name: Show info
|
- name: Show info
|
||||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||||
|
- name: Configure TSC problem matcher
|
||||||
- run: yarn type-check
|
run: |
|
||||||
|
echo "::remove-matcher owner=tsc::"
|
||||||
|
echo "::add-matcher::.github/matchers/tsc-absolute.json"
|
||||||
|
- run: yarn type-check:ci
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
name: Cron - downgradeUsers
|
name: Cron - downgradeUsers
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||||
schedule:
|
schedule:
|
||||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
# Runs “Every month at 1st (see https://crontab.guru)
|
||||||
- cron: "0,15,30,45 * * * *"
|
- cron: "0 0 1 * *"
|
||||||
jobs:
|
jobs:
|
||||||
cron-downgradeUsers:
|
cron-downgradeUsers:
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -352,7 +352,6 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
|
|||||||
</picture>
|
</picture>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
|
||||||
<!-- CONTRIBUTORS -->
|
<!-- CONTRIBUTORS -->
|
||||||
|
|
||||||
### Contributors
|
### Contributors
|
||||||
|
|||||||
@@ -11,7 +11,8 @@
|
|||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
||||||
"start": "PORT=4000 next start",
|
"start": "PORT=4000 next start",
|
||||||
"type-check": "tsc --pretty --noEmit"
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||||
},
|
},
|
||||||
"author": "Cal.com, Inc.",
|
"author": "Cal.com, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
import type { OptionProps } from "react-select";
|
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import type { App } from "@calcom/types/App";
|
|
||||||
import { Button } from "@calcom/ui";
|
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
|
||||||
|
|
||||||
interface AdditionalCalendarSelectorProps {
|
|
||||||
isLoading?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
|
|
||||||
const { data } = optionProps;
|
|
||||||
return (
|
|
||||||
<InstallAppButton
|
|
||||||
type={data.type}
|
|
||||||
render={(installProps) => {
|
|
||||||
return (
|
|
||||||
<Button {...installProps} className="w-full" color="minimal">
|
|
||||||
{/* eslint-disable @next/next/no-img-element */}
|
|
||||||
{data.image && (
|
|
||||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
|
||||||
)}
|
|
||||||
<p>{data.label}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryCell
|
|
||||||
query={query}
|
|
||||||
success={({ data }) => {
|
|
||||||
const options = data.items.map((item) => ({
|
|
||||||
label: item.name,
|
|
||||||
slug: item.slug,
|
|
||||||
image: item.logo,
|
|
||||||
type: item.type,
|
|
||||||
}));
|
|
||||||
return (
|
|
||||||
<Select
|
|
||||||
name="additionalCalendar"
|
|
||||||
placeholder={t("connect_additional_calendar")}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
color: "#3E3E3E",
|
|
||||||
marginLeft: "3px",
|
|
||||||
};
|
|
||||||
},
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "2px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
maxWidth: "320px",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm font-medium text-gray-700"
|
|
||||||
isLoading={isLoading}
|
|
||||||
components={{ Option: ImageOption }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AdditionalCalendarSelector;
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { DestinationCalendar } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onChange: (value: { externalId: string; integration: string }) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
hidePlaceholder?: boolean;
|
|
||||||
/** The external Id of the connected calendar */
|
|
||||||
value: string | undefined;
|
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DestinationCalendarSelector = ({
|
|
||||||
onChange,
|
|
||||||
isLoading,
|
|
||||||
value,
|
|
||||||
hidePlaceholder,
|
|
||||||
destinationCalendar,
|
|
||||||
}: Props): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
|
||||||
|
|
||||||
// Extra styles to show prefixed text in react-select
|
|
||||||
const content = (hidePlaceholder = false) => {
|
|
||||||
if (!hidePlaceholder) {
|
|
||||||
return {
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
":before": {
|
|
||||||
content: `'${t("select_destination_calendar")}:'`,
|
|
||||||
display: "block",
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selected = query.data?.connectedCalendars
|
|
||||||
.map((connected) => connected.calendars ?? [])
|
|
||||||
.flat()
|
|
||||||
.find((cal) => cal.externalId === value);
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
setSelectedOption({
|
|
||||||
value: `${selected.integration}:${selected.externalId}`,
|
|
||||||
label: selected.name || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [query.data?.connectedCalendars, value]);
|
|
||||||
|
|
||||||
if (!query.data?.connectedCalendars.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const options =
|
|
||||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
|
||||||
key: selectedCalendar.credentialId,
|
|
||||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
|
||||||
options: (selectedCalendar.calendars ?? [])
|
|
||||||
.filter((cal) => cal.readOnly === false)
|
|
||||||
.map((cal) => ({
|
|
||||||
label: cal.name || "",
|
|
||||||
value: `${cal.integration}:${cal.externalId}`,
|
|
||||||
})),
|
|
||||||
})) ?? [];
|
|
||||||
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
|
||||||
? `(${
|
|
||||||
destinationCalendar.externalId.length > 15
|
|
||||||
? destinationCalendar.externalId.substring(0, 15) + "..."
|
|
||||||
: destinationCalendar.externalId
|
|
||||||
})`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
<div className="relative" title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
|
||||||
<Select
|
|
||||||
name="primarySelectedCalendar"
|
|
||||||
placeholder={
|
|
||||||
!hidePlaceholder ? (
|
|
||||||
`${t("select_destination_calendar")}`
|
|
||||||
) : (
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "6px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className={classNames(
|
|
||||||
"mt-1 mb-2 block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
|
||||||
!hidePlaceholder && "font-medium"
|
|
||||||
)}
|
|
||||||
onChange={(option) => {
|
|
||||||
setSelectedOption(option);
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
|
||||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
integration,
|
|
||||||
externalId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
value={selectedOption}
|
|
||||||
components={{
|
|
||||||
IndicatorSeparator: () => null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DestinationCalendarSelector;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
import { Collapsible, CollapsibleContent } from "@radix-ui/react-collapsible";
|
||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import type { NextRouter } from "next/router";
|
import type { NextRouter } from "next/router";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
import { FiCode, FiTrello, FiSun, FiArrowLeft } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
|
|||||||
const height = getDimension(previewState.inline.height);
|
const height = getDimension(previewState.inline.height);
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
@@ -187,12 +187,12 @@ function MyComponent() {
|
|||||||
}) => {
|
}) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
const cal = await getCalApi();
|
const cal = await getCalApi();
|
||||||
Cal("floatingButton", ${floatingButtonArg});
|
cal("floatingButton", ${floatingButtonArg});
|
||||||
${uiInstructionCode}
|
${uiInstructionCode}
|
||||||
})();
|
})();
|
||||||
}, [])
|
}, [])
|
||||||
@@ -201,7 +201,7 @@ function MyComponent() {
|
|||||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||||
return code`
|
return code`
|
||||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||||
|
import { useEffect } from "react";
|
||||||
function MyComponent() {
|
function MyComponent() {
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
(async function () {
|
(async function () {
|
||||||
@@ -313,7 +313,7 @@ const embeds: {
|
|||||||
}[] = [
|
}[] = [
|
||||||
{
|
{
|
||||||
title: "Inline Embed",
|
title: "Inline Embed",
|
||||||
subtitle: "Loads your Cal scheduling page directly inline with your other website content",
|
subtitle: "Loads your event type directly inline with your other website content.",
|
||||||
type: "inline",
|
type: "inline",
|
||||||
illustration: (
|
illustration: (
|
||||||
<svg
|
<svg
|
||||||
@@ -798,7 +798,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const ThemeOptions = [
|
const ThemeOptions = [
|
||||||
{ value: Theme.auto, label: "Auto Theme" },
|
{ value: Theme.auto, label: "Auto" },
|
||||||
{ value: Theme.dark, label: "Dark Theme" },
|
{ value: Theme.dark, label: "Dark Theme" },
|
||||||
{ value: Theme.light, label: "Light Theme" },
|
{ value: Theme.light, label: "Light Theme" },
|
||||||
];
|
];
|
||||||
@@ -815,11 +815,18 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DialogContent ref={dialogContentRef} size="xl" className="p-0.5" type="creation">
|
<DialogContent
|
||||||
|
ref={dialogContentRef}
|
||||||
|
className="rounded-lg p-0.5 sm:max-w-[80rem]"
|
||||||
|
enableOverflow
|
||||||
|
type="creation">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
<div className="flex w-1/3 flex-col bg-gray-50 p-8">
|
||||||
<h3 className="mb-2 flex text-xl font-bold leading-6 text-gray-900" id="modal-title">
|
<h3
|
||||||
|
className="mb-2.5 flex items-center text-xl font-semibold leading-5 text-gray-900"
|
||||||
|
id="modal-title">
|
||||||
<button
|
<button
|
||||||
|
className="h-6 w-6"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||||
}}>
|
}}>
|
||||||
@@ -827,35 +834,21 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</button>
|
</button>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
|
<h4 className="mb-6 text-sm font-normal text-gray-500">{embed.subtitle}</h4>
|
||||||
<div className="flex flex-col overflow-y-auto">
|
<div className="flex flex-col">
|
||||||
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
|
<div className={classNames("font-medium", embedType === "element-click" ? "hidden" : "")}>
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isEmbedCustomizationOpen}
|
open={isEmbedCustomizationOpen}
|
||||||
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsEmbedCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger
|
|
||||||
type="button"
|
|
||||||
className="flex w-full items-center text-base font-medium text-gray-900">
|
|
||||||
<div>
|
|
||||||
{embedType === "inline"
|
|
||||||
? "Inline Embed Customization"
|
|
||||||
: embedType === "floating-popup"
|
|
||||||
? "Floating Popup Customization"
|
|
||||||
: "Element Click Customization"}
|
|
||||||
</div>
|
|
||||||
<FiChevronRight
|
|
||||||
className={`${
|
|
||||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
|
||||||
/>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent className="text-sm">
|
<CollapsibleContent className="text-sm">
|
||||||
<div className={classNames("mt-6", embedType === "inline" ? "block" : "hidden")}>
|
<div className={classNames(embedType === "inline" ? "block" : "hidden")}>
|
||||||
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
{/*TODO: Add Auto/Fixed toggle from Figma */}
|
||||||
<div className="text-sm">Embed Window Sizing</div>
|
<div className="mb-[9px] text-sm">Window sizing</div>
|
||||||
<div className="justify-left flex items-center">
|
<div className="justify-left flex items-center !font-normal">
|
||||||
|
<div className="mr-[9px]">
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
|
className="focus:ring-offset-0"
|
||||||
required
|
required
|
||||||
value={previewState.inline.width}
|
value={previewState.inline.width}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -873,9 +866,11 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
}}
|
}}
|
||||||
addOnLeading={<>W</>}
|
addOnLeading={<>W</>}
|
||||||
/>
|
/>
|
||||||
<span className="p-2">×</span>
|
</div>
|
||||||
|
|
||||||
<TextField
|
<TextField
|
||||||
labelProps={{ className: "hidden" }}
|
labelProps={{ className: "hidden" }}
|
||||||
|
className="focus:ring-offset-0"
|
||||||
value={previewState.inline.height}
|
value={previewState.inline.height}
|
||||||
required
|
required
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
@@ -1007,22 +1002,13 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
</div>
|
</div>
|
||||||
<hr className="mt-4" />
|
<div className="font-medium">
|
||||||
<div className="mt-4 font-medium">
|
|
||||||
<Collapsible
|
<Collapsible
|
||||||
open={isBookingCustomizationOpen}
|
open={isBookingCustomizationOpen}
|
||||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||||
<CollapsibleTrigger className="flex w-full" type="button">
|
|
||||||
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
|
|
||||||
<FiChevronRight
|
|
||||||
className={`${
|
|
||||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
|
||||||
} ml-auto h-5 w-5 text-gray-500`}
|
|
||||||
/>
|
|
||||||
</CollapsibleTrigger>
|
|
||||||
<CollapsibleContent>
|
<CollapsibleContent>
|
||||||
<div className="mt-6 text-sm">
|
<div className="mt-6 text-sm">
|
||||||
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
<div className="mb-6 flex items-center justify-start space-x-2 rtl:space-x-reverse">
|
||||||
<Switch
|
<Switch
|
||||||
checked={previewState.hideEventTypeDetails}
|
checked={previewState.hideEventTypeDetails}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
@@ -1036,13 +1022,38 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
/>
|
/>
|
||||||
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
<div className="text-sm">{t("hide_eventtype_details")}</div>
|
||||||
</div>
|
</div>
|
||||||
<Label className="">
|
{[
|
||||||
|
{ name: "brandColor", title: "Brand Color" },
|
||||||
|
// { name: "lightColor", title: "Light Color" },
|
||||||
|
// { name: "lighterColor", title: "Lighter Color" },
|
||||||
|
// { name: "lightestColor", title: "Lightest Color" },
|
||||||
|
// { name: "highlightColor", title: "Highlight Color" },
|
||||||
|
// { name: "medianColor", title: "Median Color" },
|
||||||
|
].map((palette) => (
|
||||||
|
<Label key={palette.name} className="mb-6">
|
||||||
|
<div className="mb-2">{palette.title}</div>
|
||||||
|
<div className="w-full">
|
||||||
|
<ColorPicker
|
||||||
|
popoverAlign="start"
|
||||||
|
container={dialogContentRef?.current ?? undefined}
|
||||||
|
defaultValue="#000000"
|
||||||
|
onChange={(color) => {
|
||||||
|
addToPalette({
|
||||||
|
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
))}
|
||||||
|
<Label>
|
||||||
<div className="mb-2">Theme</div>
|
<div className="mb-2">Theme</div>
|
||||||
<Select
|
<Select
|
||||||
className="w-full"
|
className="w-full"
|
||||||
defaultValue={ThemeOptions[0]}
|
defaultValue={ThemeOptions[0]}
|
||||||
components={{
|
components={{
|
||||||
Control: ThemeSelectControl,
|
Control: ThemeSelectControl,
|
||||||
|
IndicatorSeparator: () => null,
|
||||||
}}
|
}}
|
||||||
onChange={(option) => {
|
onChange={(option) => {
|
||||||
if (!option) {
|
if (!option) {
|
||||||
@@ -1058,30 +1069,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
options={ThemeOptions}
|
options={ThemeOptions}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
{[
|
|
||||||
{ name: "brandColor", title: "Brand Color" },
|
|
||||||
// { name: "lightColor", title: "Light Color" },
|
|
||||||
// { name: "lighterColor", title: "Lighter Color" },
|
|
||||||
// { name: "lightestColor", title: "Lightest Color" },
|
|
||||||
// { name: "highlightColor", title: "Highlight Color" },
|
|
||||||
// { name: "medianColor", title: "Median Color" },
|
|
||||||
].map((palette) => (
|
|
||||||
<Label key={palette.name} className="pb-4">
|
|
||||||
<div className="mb-2 pt-2">{palette.title}</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<ColorPicker
|
|
||||||
popoverAlign="start"
|
|
||||||
container={dialogContentRef?.current ?? undefined}
|
|
||||||
defaultValue="#000000"
|
|
||||||
onChange={(color) => {
|
|
||||||
addToPalette({
|
|
||||||
[palette.name as keyof (typeof previewState)["palette"]]: color,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Label>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
</Collapsible>
|
</Collapsible>
|
||||||
|
|||||||
@@ -1,10 +1,14 @@
|
|||||||
import type { OptionProps } from "react-select";
|
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App } from "@calcom/types/App";
|
import {
|
||||||
import { Button, Select } from "@calcom/ui";
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownItem,
|
||||||
|
} from "@calcom/ui";
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
import { FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
@@ -13,31 +17,6 @@ interface AdditionalCalendarSelectorProps {
|
|||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App["type"] }>) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const { data } = optionProps;
|
|
||||||
return data.slug !== "add-new" ? (
|
|
||||||
<InstallAppButton
|
|
||||||
type={data.type}
|
|
||||||
render={(installProps) => {
|
|
||||||
return (
|
|
||||||
<Button {...installProps} className="flex w-full align-top" color="minimal">
|
|
||||||
{data.image && (
|
|
||||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
|
||||||
)}
|
|
||||||
<p className="text-left">{`${t("add")} ${data.label}`}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
|
|
||||||
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
|
||||||
<p>{t("install_new_calendar_app")}</p>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||||
@@ -59,51 +38,39 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
type: "new_other",
|
type: "new_other",
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<Select
|
<Dropdown modal={false}>
|
||||||
name="additionalCalendar"
|
<DropdownMenuTrigger asChild>
|
||||||
placeholder={
|
<Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
||||||
<Button StartIcon={FiPlus} color="secondary">
|
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
</DropdownMenuTrigger>
|
||||||
options={options}
|
<DropdownMenuContent>
|
||||||
isSearchable={false}
|
{options.map((data) => (
|
||||||
isLoading={isLoading}
|
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||||
components={{ Option: ImageOption }}
|
{data.slug === "add-new" ? (
|
||||||
styles={{
|
<DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
|
||||||
menu: (defaultStyles) => ({
|
{t("install_new_calendar_app")}
|
||||||
...defaultStyles,
|
</DropdownItem>
|
||||||
"@media only screen and (max-width: 640px)": {
|
) : (
|
||||||
left: "0",
|
<InstallAppButton
|
||||||
},
|
type={data.type}
|
||||||
width: "max-content",
|
render={(installProps) => {
|
||||||
right: "0",
|
const props = { ...installProps } as any;
|
||||||
}),
|
return (
|
||||||
indicatorSeparator: (defaultStyles) => ({
|
<DropdownItem {...props} color="minimal" type="button">
|
||||||
...defaultStyles,
|
<span className="flex items-center gap-x-2">
|
||||||
display: "none",
|
{data.image && <img className="h-5 w-5" src={data.image} alt={data.label} />}
|
||||||
}),
|
{`${t("add")} ${data.label}`}
|
||||||
control: (defaultStyles) => ({
|
</span>
|
||||||
...defaultStyles,
|
</DropdownItem>
|
||||||
padding: "0",
|
);
|
||||||
border: "0",
|
|
||||||
borderRadius: "6px",
|
|
||||||
minHeight: "auto",
|
|
||||||
}),
|
|
||||||
dropdownIndicator: (defaultStyles) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
display: "none",
|
|
||||||
}),
|
|
||||||
valueContainer: (defaultStyles) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
padding: "0",
|
|
||||||
}),
|
|
||||||
placeholder: (defaultStyles) => ({
|
|
||||||
...defaultStyles,
|
|
||||||
margin: "0",
|
|
||||||
}),
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</Dropdown>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,139 +0,0 @@
|
|||||||
import classNames from "classnames";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import Select from "react-select";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { DestinationCalendar } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
onChange: (value: { externalId: string; integration: string }) => void;
|
|
||||||
isLoading?: boolean;
|
|
||||||
hidePlaceholder?: boolean;
|
|
||||||
/** The external Id of the connected calendar */
|
|
||||||
value: string | undefined;
|
|
||||||
destinationCalendar?: DestinationCalendar | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DestinationCalendarSelector = ({
|
|
||||||
onChange,
|
|
||||||
isLoading,
|
|
||||||
value,
|
|
||||||
hidePlaceholder,
|
|
||||||
destinationCalendar,
|
|
||||||
}: Props): JSX.Element | null => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const query = trpc.viewer.connectedCalendars.useQuery();
|
|
||||||
const [selectedOption, setSelectedOption] = useState<{ value: string; label: string } | null>(null);
|
|
||||||
|
|
||||||
// Extra styles to show prefixed text in react-select
|
|
||||||
const content = (hidePlaceholder = false) => {
|
|
||||||
if (!hidePlaceholder) {
|
|
||||||
return {
|
|
||||||
alignItems: "center",
|
|
||||||
display: "flex",
|
|
||||||
":before": {
|
|
||||||
content: `'${t("select_destination_calendar")}:'`,
|
|
||||||
display: "block",
|
|
||||||
marginRight: 8,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return {};
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const selected = query.data?.connectedCalendars
|
|
||||||
.map((connected) => connected.calendars ?? [])
|
|
||||||
.flat()
|
|
||||||
.find((cal) => cal.externalId === value);
|
|
||||||
|
|
||||||
if (selected) {
|
|
||||||
setSelectedOption({
|
|
||||||
value: `${selected.integration}:${selected.externalId}`,
|
|
||||||
label: `${selected.name} (${selected.externalId})` || "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [query.data?.connectedCalendars, value]);
|
|
||||||
|
|
||||||
if (!query.data?.connectedCalendars.length) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const options =
|
|
||||||
query.data.connectedCalendars.map((selectedCalendar) => ({
|
|
||||||
key: selectedCalendar.credentialId,
|
|
||||||
label: `${selectedCalendar.integration.title} (${selectedCalendar.primary?.name})`,
|
|
||||||
options: (selectedCalendar.calendars ?? [])
|
|
||||||
.filter((cal) => cal.readOnly === false)
|
|
||||||
.map((cal) => ({
|
|
||||||
label: `${cal.name} (${cal.externalId})` || "",
|
|
||||||
value: `${cal.integration}:${cal.externalId}`,
|
|
||||||
})),
|
|
||||||
})) ?? [];
|
|
||||||
const defaultCalendarSelectedString = destinationCalendar?.externalId
|
|
||||||
? `(${
|
|
||||||
destinationCalendar.externalId.length > 15
|
|
||||||
? destinationCalendar.externalId.substring(0, 15) + "..."
|
|
||||||
: destinationCalendar.externalId
|
|
||||||
})`
|
|
||||||
: "";
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
className="relative flex items-center"
|
|
||||||
title={`${t("select_destination_calendar")}: ${selectedOption?.label || ""}`}>
|
|
||||||
<Select
|
|
||||||
name="primarySelectedCalendar"
|
|
||||||
placeholder={
|
|
||||||
!hidePlaceholder ? (
|
|
||||||
`${t("select_destination_calendar")}`
|
|
||||||
) : (
|
|
||||||
<span className="whitespace-nowrap">
|
|
||||||
{t("default_calendar_selected")} {defaultCalendarSelectedString}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
options={options}
|
|
||||||
styles={{
|
|
||||||
placeholder: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
singleValue: (styles) => ({ ...styles, ...content(hidePlaceholder) }),
|
|
||||||
control: (defaultStyles) => {
|
|
||||||
return {
|
|
||||||
...defaultStyles,
|
|
||||||
borderRadius: "6px",
|
|
||||||
"@media only screen and (min-width: 640px)": {
|
|
||||||
...(defaultStyles["@media only screen and (min-width: 640px)"] as object),
|
|
||||||
width: "100%",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
isSearchable={false}
|
|
||||||
className={classNames(
|
|
||||||
"block w-full min-w-0 flex-1 rounded-none rounded-r-sm border-gray-300 text-sm",
|
|
||||||
!hidePlaceholder && "font-medium"
|
|
||||||
)}
|
|
||||||
onChange={(option) => {
|
|
||||||
setSelectedOption(option);
|
|
||||||
if (!option) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Split only the first `:`, since Apple uses the full URL as externalId */
|
|
||||||
const [integration, externalId] = option.value.split(/:(.+)/);
|
|
||||||
|
|
||||||
onChange({
|
|
||||||
integration,
|
|
||||||
externalId,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
isLoading={isLoading}
|
|
||||||
value={selectedOption}
|
|
||||||
components={{
|
|
||||||
IndicatorSeparator: () => null,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default DestinationCalendarSelector;
|
|
||||||
@@ -8,17 +8,20 @@ import { FiLink } from "@calcom/ui/components/icon";
|
|||||||
|
|
||||||
import type { Props } from "./pages/AvailabilityPage";
|
import type { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
|
const excludeNullValues = (value: unknown) => !!value;
|
||||||
|
|
||||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return locations.length ? (
|
const renderLocations = locations.map((location, index) => {
|
||||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
|
||||||
{locations.map((location, index) => {
|
|
||||||
const eventLocationType = getEventLocationType(location.type);
|
const eventLocationType = getEventLocationType(location.type);
|
||||||
if (!eventLocationType) {
|
if (!eventLocationType) {
|
||||||
// It's possible that the location app got uninstalled
|
// It's possible that the location app got uninstalled
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (eventLocationType.variable === "hostDefault") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const translateAbleKeys = [
|
const translateAbleKeys = [
|
||||||
"attendee_in_person",
|
"attendee_in_person",
|
||||||
@@ -29,7 +32,6 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
];
|
];
|
||||||
|
|
||||||
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
||||||
|
|
||||||
const translatedLocation = location.type.startsWith("integrations:")
|
const translatedLocation = location.type.startsWith("integrations:")
|
||||||
? eventLocationType.label
|
? eventLocationType.label
|
||||||
: translateAbleKeys.includes(locationKey)
|
: translateAbleKeys.includes(locationKey)
|
||||||
@@ -55,7 +57,12 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
});
|
||||||
|
|
||||||
|
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
|
||||||
|
return filteredLocations.length ? (
|
||||||
|
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
||||||
|
{filteredLocations}
|
||||||
</div>
|
</div>
|
||||||
) : null;
|
) : null;
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,7 @@ interface ISetLocationDialog {
|
|||||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
||||||
selection?: LocationOption;
|
selection?: LocationOption;
|
||||||
booking?: BookingItem;
|
booking?: BookingItem;
|
||||||
|
isTeamEvent?: boolean;
|
||||||
defaultValues?: LocationObject[];
|
defaultValues?: LocationObject[];
|
||||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -74,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
saveLocation,
|
saveLocation,
|
||||||
selection,
|
selection,
|
||||||
booking,
|
booking,
|
||||||
|
isTeamEvent,
|
||||||
setShowLocationModal,
|
setShowLocationModal,
|
||||||
isOpenDialog,
|
isOpenDialog,
|
||||||
defaultValues,
|
defaultValues,
|
||||||
@@ -290,7 +292,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
query={locationsQuery}
|
query={locationsQuery}
|
||||||
success={({ data }) => {
|
success={({ data }) => {
|
||||||
if (!data.length) return null;
|
if (!data.length) return null;
|
||||||
const locationOptions = [...data];
|
const locationOptions = [...data].filter((option) => {
|
||||||
|
return !isTeamEvent ? option.label !== "Conferencing" : true;
|
||||||
|
});
|
||||||
if (booking) {
|
if (booking) {
|
||||||
locationOptions.map((location) =>
|
locationOptions.map((location) =>
|
||||||
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
|
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
|
||||||
|
|||||||
@@ -426,6 +426,7 @@ const IntervalLimitItem = ({
|
|||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
||||||
|
className="mb-0"
|
||||||
placeholder={`${value}`}
|
placeholder={`${value}`}
|
||||||
min={step}
|
min={step}
|
||||||
step={step}
|
step={step}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { isValidPhoneNumber } from "libphonenumber-js";
|
|||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import type { MultiValue } from "react-select";
|
import type { MultiValue } from "react-select";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -16,7 +16,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
import { slugify } from "@calcom/lib/slugify";
|
import { slugify } from "@calcom/lib/slugify";
|
||||||
import turndown from "@calcom/lib/turndownService";
|
import turndown from "@calcom/lib/turndownService";
|
||||||
import { Button, Editor, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
import {
|
||||||
|
Button,
|
||||||
|
Label,
|
||||||
|
Select,
|
||||||
|
SettingsToggle,
|
||||||
|
Skeleton,
|
||||||
|
TextField,
|
||||||
|
Editor,
|
||||||
|
SkeletonContainer,
|
||||||
|
SkeletonText,
|
||||||
|
} from "@calcom/ui";
|
||||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
@@ -35,14 +45,31 @@ const getLocationFromType = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
interface DescriptionEditorProps {
|
||||||
for (const locationType of options) {
|
description?: string | null;
|
||||||
for (const location of locationType.options) {
|
|
||||||
if (location.value === type && location.disabled === false) {
|
|
||||||
return location;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DescriptionEditor = (props: DescriptionEditorProps) => {
|
||||||
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
const [mounted, setIsMounted] = useState(false);
|
||||||
|
const { t } = useLocale();
|
||||||
|
const { description } = props;
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return mounted ? (
|
||||||
|
<Editor
|
||||||
|
getText={() => md.render(formMethods.getValues("description") || description || "")}
|
||||||
|
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||||
|
excludedToolbarItems={["blockType"]}
|
||||||
|
placeholder={t("quick_video_meeting")}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<SkeletonContainer>
|
||||||
|
<SkeletonText className="block h-24 w-full" />
|
||||||
|
</SkeletonContainer>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventSetupTab = (
|
export const EventSetupTab = (
|
||||||
@@ -53,12 +80,16 @@ export const EventSetupTab = (
|
|||||||
) => {
|
) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { eventType, locationOptions, team, destinationCalendar } = props;
|
const { eventType, team, destinationCalendar } = props;
|
||||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
||||||
|
|
||||||
|
const locationOptions = props.locationOptions.filter((option) => {
|
||||||
|
return !team ? option.label !== "Conferencing" : true;
|
||||||
|
});
|
||||||
|
|
||||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||||
value: mins,
|
value: mins,
|
||||||
label: t("multiple_duration_mins", { count: mins }),
|
label: t("multiple_duration_mins", { count: mins }),
|
||||||
@@ -287,12 +318,7 @@ export const EventSetupTab = (
|
|||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Label>{t("description")}</Label>
|
<Label>{t("description")}</Label>
|
||||||
<Editor
|
<DescriptionEditor description={eventType?.description} />
|
||||||
getText={() => md.render(formMethods.getValues("description") || eventType.description || "")}
|
|
||||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
|
||||||
excludedToolbarItems={["blockType"]}
|
|
||||||
placeholder={t("quick_video_meeting")}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
@@ -409,6 +435,7 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
||||||
<EditLocationDialog
|
<EditLocationDialog
|
||||||
|
isTeamEvent={!!team}
|
||||||
isOpenDialog={showLocationModal}
|
isOpenDialog={showLocationModal}
|
||||||
setShowLocationModal={setShowLocationModal}
|
setShowLocationModal={setShowLocationModal}
|
||||||
saveLocation={saveLocation}
|
saveLocation={saveLocation}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
export type EventTypeDescriptionSafeProps = {
|
export type EventTypeDescriptionSafeProps = {
|
||||||
eventType: { description: string | null };
|
eventType: { description: string | null; descriptionAsSafeHTML: string | null };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
||||||
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
||||||
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
|
if (eventType.description)
|
||||||
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
|
props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
|
||||||
return <div {...props} />;
|
return <div {...props} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { RouterInputs } from "@calcom/trpc/react";
|
import type { RouterInputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";
|
|
||||||
|
|
||||||
interface ICreateEventsOnCalendarSelectProps {
|
interface ICreateEventsOnCalendarSelectProps {
|
||||||
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
|
calendar?: RouterInputs["viewer"]["setDestinationCalendar"] | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,14 @@ import Link from "next/link";
|
|||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
|
||||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
|
|
||||||
type TeamType = NonNullable<TeamWithMembers>;
|
type TeamType = NonNullable<TeamWithMembers>;
|
||||||
type MembersType = TeamType["members"];
|
type MembersType = TeamType["members"];
|
||||||
type MemberType = MembersType[number];
|
type MemberType = MembersType[number] & { safeBio: string | null };
|
||||||
|
|
||||||
|
type TeamTypeWithSafeHtml = Omit<TeamType, "members"> & { members: MemberType[] };
|
||||||
|
|
||||||
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -30,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: member.safeBio || "" }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
@@ -43,7 +44,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Members = ({ members, teamName }: { members: MembersType; teamName: string | null }) => {
|
const Members = ({ members, teamName }: { members: MemberType[]; teamName: string | null }) => {
|
||||||
if (!members || members.length === 0) {
|
if (!members || members.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -57,7 +58,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const Team = ({ team }: { team: TeamType }) => {
|
const Team = ({ team }: { team: TeamTypeWithSafeHtml }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Members members={team.members} teamName={team.name} />
|
<Members members={team.members} teamName={team.name} />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.7.5",
|
"version": "2.7.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -11,6 +11,7 @@
|
|||||||
"dx": "yarn dev",
|
"dx": "yarn dev",
|
||||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint . --ignore-path .gitignore",
|
"lint": "eslint . --ignore-path .gitignore",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
"@vercel/edge-config": "^0.1.1",
|
"@vercel/edge-config": "^0.1.1",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
"@vercel/og": "^0.0.21",
|
"@vercel/og": "^0.5.0",
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
@@ -119,6 +120,7 @@
|
|||||||
"react-window": "^1.8.7",
|
"react-window": "^1.8.7",
|
||||||
"remark": "^14.0.2",
|
"remark": "^14.0.2",
|
||||||
"rrule": "^2.7.1",
|
"rrule": "^2.7.1",
|
||||||
|
"sanitize-html": "^2.10.0",
|
||||||
"schema-dts": "^1.1.0",
|
"schema-dts": "^1.1.0",
|
||||||
"short-uuid": "^4.2.0",
|
"short-uuid": "^4.2.0",
|
||||||
"strip-markdown": "^5.0.0",
|
"strip-markdown": "^5.0.0",
|
||||||
@@ -156,6 +158,7 @@
|
|||||||
"@types/react-phone-number-input": "^3.0.14",
|
"@types/react-phone-number-input": "^3.0.14",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
|
"@types/sanitize-html": "^2.9.0",
|
||||||
"@types/stripe": "^8.0.417",
|
"@types/stripe": "^8.0.417",
|
||||||
"@types/uuid": "8.3.1",
|
"@types/uuid": "8.3.1",
|
||||||
"autoprefixer": "^10.4.12",
|
"autoprefixer": "^10.4.12",
|
||||||
|
|||||||
+38
-1
@@ -6,7 +6,7 @@ import { useEffect, useState } from "react";
|
|||||||
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
import { FiFileText, FiCheck, FiBookOpen, FiChevronRight } from "@calcom/ui/components/icon";
|
import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
@@ -40,6 +40,43 @@ export default function Custom404() {
|
|||||||
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
||||||
const isSignup = router.asPath.startsWith("/signup");
|
const isSignup = router.asPath.startsWith("/signup");
|
||||||
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
|
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
|
||||||
|
/**
|
||||||
|
* If we're on 404 and the route is insights it means it is disabled
|
||||||
|
* TODO: Abstract this for all disabled features
|
||||||
|
**/
|
||||||
|
const isInsights = router.asPath.startsWith("/insights");
|
||||||
|
if (isInsights) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<HeadSeo
|
||||||
|
title="Feature is currently disabled"
|
||||||
|
description={t("404_page_not_found")}
|
||||||
|
nextSeoProps={{
|
||||||
|
nofollow: true,
|
||||||
|
noindex: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="min-h-screen bg-white px-4" data-testid="404-page">
|
||||||
|
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
|
||||||
|
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
|
||||||
|
Feature is currently disabled
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div className="mt-12">
|
||||||
|
<div className="mt-8">
|
||||||
|
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
|
||||||
|
{t("or_go_back_home")}
|
||||||
|
<span aria-hidden="true"> →</span>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import defaultEvents, {
|
|||||||
} from "@calcom/lib/defaultEvents";
|
} from "@calcom/lib/defaultEvents";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
@@ -147,7 +147,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -343,6 +343,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
const eventTypes = eventTypesRaw.map((eventType) => ({
|
const eventTypes = eventTypesRaw.map((eventType) => ({
|
||||||
...eventType,
|
...eventType,
|
||||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const isSingleUser = users.length === 1;
|
const isSingleUser = users.length === 1;
|
||||||
@@ -352,9 +353,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
})
|
})
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
users,
|
users,
|
||||||
|
safeBio,
|
||||||
profile,
|
profile,
|
||||||
user: {
|
user: {
|
||||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
|||||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { addListFormatting } from "@calcom/lib/markdownIt";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import type { User } from "@calcom/prisma/client";
|
import type { User } from "@calcom/prisma/client";
|
||||||
|
|
||||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||||
@@ -59,7 +59,6 @@ Type.isThemeSupported = true;
|
|||||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
async function getUserPageProps(context: GetStaticPropsContext) {
|
||||||
// load server side dependencies
|
// load server side dependencies
|
||||||
const MarkdownIt = await import("markdown-it").then((mod) => mod.default);
|
|
||||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||||
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
||||||
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
||||||
@@ -124,9 +123,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
|
||||||
|
|
||||||
if (!user || !user.eventTypes.length) return { notFound: true };
|
if (!user || !user.eventTypes.length) return { notFound: true };
|
||||||
|
|
||||||
const [eventType]: ((typeof user.eventTypes)[number] & {
|
const [eventType]: ((typeof user.eventTypes)[number] & {
|
||||||
@@ -153,7 +149,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||||
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
||||||
locations: privacyFilteredLocations(locations),
|
locations: privacyFilteredLocations(locations),
|
||||||
descriptionAsSafeHTML: eventType.description ? addListFormatting(md.render(eventType.description)) : null,
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
});
|
});
|
||||||
// Check if the user you are logging into has any active teams or premium user name
|
// Check if the user you are logging into has any active teams or premium user name
|
||||||
const hasActiveTeam =
|
const hasActiveTeam =
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
getUsernameList,
|
getUsernameList,
|
||||||
} from "@calcom/lib/defaultEvents";
|
} from "@calcom/lib/defaultEvents";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
||||||
import {
|
import {
|
||||||
customInputSchema,
|
customInputSchema,
|
||||||
@@ -189,6 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
slug: u.username,
|
slug: u.username,
|
||||||
theme: u.theme,
|
theme: u.theme,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
};
|
};
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,12 @@
|
|||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
page: z.coerce.number().min(0).optional().default(0),
|
||||||
|
});
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||||
@@ -11,10 +19,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||||
* TODO:
|
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
|
||||||
* Remove this endpoint
|
let { page: pageNumber } = querySchema.parse(req.query);
|
||||||
*/
|
|
||||||
|
while (true) {
|
||||||
|
const teams = await prisma.team.findMany({
|
||||||
|
where: {
|
||||||
|
slug: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
},
|
||||||
|
skip: pageNumber * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (teams.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const team of teams) {
|
||||||
|
await updateQuantitySubscriptionFromStripe(team.id);
|
||||||
|
await delay(100); // Adjust the delay as needed to avoid rate limiting
|
||||||
|
}
|
||||||
|
|
||||||
|
pageNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
}
|
}
|
||||||
@@ -88,7 +88,7 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
ogConfig
|
ogConfig
|
||||||
) as { body: Buffer };
|
) as { body: Buffer };
|
||||||
|
|
||||||
return new Response(img.body, { status: 200 });
|
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||||
}
|
}
|
||||||
case "app": {
|
case "app": {
|
||||||
const { name, description, slug } = appSchema.parse({
|
const { name, description, slug } = appSchema.parse({
|
||||||
@@ -101,7 +101,7 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
body: Buffer;
|
body: Buffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Response(img.body, { status: 200 });
|
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||||
}
|
}
|
||||||
|
|
||||||
case "generic": {
|
case "generic": {
|
||||||
@@ -115,7 +115,7 @@ export default async function handler(req: NextApiRequest) {
|
|||||||
body: Buffer;
|
body: Buffer;
|
||||||
};
|
};
|
||||||
|
|
||||||
return new Response(img.body, { status: 200 });
|
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { FiSearch } from "@calcom/ui/components/icon";
|
|||||||
|
|
||||||
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
const tabs: HorizontalTabItemProps[] = [
|
const tabs: HorizontalTabItemProps[] = [
|
||||||
{
|
{
|
||||||
@@ -88,7 +88,7 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
|
|||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { req, res } = context;
|
const { req, res } = context;
|
||||||
|
|
||||||
const ssg = await ssgInit(context);
|
const ssr = await ssrInit(context);
|
||||||
|
|
||||||
const session = await getServerSession({ req, res });
|
const session = await getServerSession({ req, res });
|
||||||
|
|
||||||
@@ -119,7 +119,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
return b.count - a.count;
|
return b.count - a.count;
|
||||||
}),
|
}),
|
||||||
appStore,
|
appStore,
|
||||||
trpcState: ssg.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -569,7 +569,8 @@ export default function Success(props: SuccessProps) {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="text-bookingdark dark:border-darkgray-200 mt-8 text-left dark:text-gray-300">
|
||||||
{Object.entries(bookingInfo.responses).map(([name, response]) => {
|
{Object.entries(bookingInfo.responses).map(([name, response]) => {
|
||||||
const field = eventType.bookingFields.find((field) => field.name === name);
|
const field = eventType.bookingFields.find((field) => field.name === name);
|
||||||
// We show location in the "where" section
|
// We show location in the "where" section
|
||||||
@@ -584,15 +585,10 @@ export default function Success(props: SuccessProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="mt-9 font-medium">{label}</div>
|
<div className="mt-4 font-medium">{label}</div>
|
||||||
<div className="col-span-2 mb-2 mt-9">
|
<p className="break-words" data-testid="field-response" data-fob-field={field.name}>
|
||||||
<p
|
|
||||||
className="break-words"
|
|
||||||
data-testid="field-response"
|
|
||||||
data-fob-field={field.name}>
|
|
||||||
{response.toString()}
|
{response.toString()}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { LocationObject } from "@calcom/core/location";
|
|||||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getWorkingHours } from "@calcom/lib/availability";
|
import { getWorkingHours } from "@calcom/lib/availability";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
|
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
@@ -119,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
hideBranding: u.hideBranding,
|
hideBranding: u.hideBranding,
|
||||||
timeZone: u.timeZone,
|
timeZone: u.timeZone,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(hashedLink.eventType.description),
|
||||||
});
|
});
|
||||||
|
|
||||||
const [user] = users;
|
const [user] = users;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
@@ -93,6 +94,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
brandColor: u.brandColor,
|
brandColor: u.brandColor,
|
||||||
darkBrandColor: u.darkBrandColor,
|
darkBrandColor: u.darkBrandColor,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
};
|
};
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ import type { EventLocationType } from "@calcom/core/location";
|
|||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import getEventTypeById from "@calcom/lib/getEventTypeById";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
@@ -65,6 +63,7 @@ export type FormValues = {
|
|||||||
hostPhoneNumber?: string;
|
hostPhoneNumber?: string;
|
||||||
displayLocationPublicly?: boolean;
|
displayLocationPublicly?: boolean;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
hostDefault?: string;
|
||||||
}[];
|
}[];
|
||||||
customInputs: CustomInputParsed[];
|
customInputs: CustomInputParsed[];
|
||||||
schedule: number | null;
|
schedule: number | null;
|
||||||
@@ -365,9 +364,9 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
|
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||||
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
|
const { data } = trpc.viewer.eventTypes.get.useQuery({ id: props.type });
|
||||||
if (isLoading || !data) return null;
|
|
||||||
return <EventTypePage {...data} />;
|
return <EventTypePage {...(data as EventTypeSetupProps)} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
@@ -393,26 +392,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
await ssr.viewer.eventTypes.get.prefetch({ id: typeParam });
|
||||||
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
// session,
|
|
||||||
type: typeParam,
|
type: typeParam,
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
initialData: {
|
|
||||||
eventType: res.eventType,
|
|
||||||
locationOptions: res.locationOptions,
|
|
||||||
team: res.team,
|
|
||||||
teamMembers: res.teamMembers,
|
|
||||||
currentUserMembership: res.currentUserMembership,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
} catch (err) {
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default EventTypePageWrapper;
|
export default EventTypePageWrapper;
|
||||||
@@ -141,7 +141,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
|||||||
</div>
|
</div>
|
||||||
<EventTypeDescription
|
<EventTypeDescription
|
||||||
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||||
eventType={type}
|
eventType={{ ...type, descriptionAsSafeHTML: type.safeDescription }}
|
||||||
shortenDescription
|
shortenDescription
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||||
import {
|
import {
|
||||||
AverageEventDurationChart,
|
AverageEventDurationChart,
|
||||||
BookingKPICards,
|
BookingKPICards,
|
||||||
@@ -39,17 +40,17 @@ export default function InsightsPage() {
|
|||||||
const { data: user } = trpc.viewer.me.useQuery();
|
const { data: user } = trpc.viewer.me.useQuery();
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
icon: <FiUsers className="h-5 w-5 text-red-500" />,
|
icon: <FiUsers className="h-5 w-5" />,
|
||||||
title: t("view_bookings_across"),
|
title: t("view_bookings_across"),
|
||||||
description: t("view_bookings_across_description"),
|
description: t("view_bookings_across_description"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <FiRefreshCcw className="h-5 w-5 text-blue-500" />,
|
icon: <FiRefreshCcw className="h-5 w-5" />,
|
||||||
title: t("identify_booking_trends"),
|
title: t("identify_booking_trends"),
|
||||||
description: t("identify_booking_trends_description"),
|
description: t("identify_booking_trends_description"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <FiUserPlus className="h-5 w-5 text-green-500" />,
|
icon: <FiUserPlus className="h-5 w-5" />,
|
||||||
title: t("spot_popular_event_types"),
|
title: t("spot_popular_event_types"),
|
||||||
description: t("spot_popular_event_types_description"),
|
description: t("spot_popular_event_types_description"),
|
||||||
},
|
},
|
||||||
@@ -62,7 +63,7 @@ export default function InsightsPage() {
|
|||||||
title={t("make_informed_decisions")}
|
title={t("make_informed_decisions")}
|
||||||
description={t("make_informed_decisions_description")}
|
description={t("make_informed_decisions_description")}
|
||||||
features={features}
|
features={features}
|
||||||
background="/banners/insights.jpg"
|
background="/tips/insights.jpg"
|
||||||
buttons={
|
buttons={
|
||||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
@@ -114,3 +115,19 @@ export default function InsightsPage() {
|
|||||||
</div>
|
</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: {},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -3,7 +3,7 @@ import { useState } from "react";
|
|||||||
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText, Alert } from "@calcom/ui";
|
||||||
|
|
||||||
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
|
||||||
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
|
||||||
@@ -32,11 +32,14 @@ const TwoFactorAuthView = () => {
|
|||||||
|
|
||||||
if (isLoading) return <SkeletonLoader />;
|
if (isLoading) return <SkeletonLoader />;
|
||||||
|
|
||||||
|
const isCalProvider = user?.identityProvider === "CAL";
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title={t("2fa")} description={t("2fa_description")} />
|
<Meta title={t("2fa")} description={t("2fa_description")} />
|
||||||
|
{!isCalProvider && <Alert severity="neutral" message={t("2fa_disabled")} />}
|
||||||
<div className="mt-6 flex items-start space-x-4">
|
<div className="mt-6 flex items-start space-x-4">
|
||||||
<Switch
|
<Switch
|
||||||
|
disabled={!isCalProvider}
|
||||||
checked={user?.twoFactorEnabled}
|
checked={user?.twoFactorEnabled}
|
||||||
onCheckedChange={() =>
|
onCheckedChange={() =>
|
||||||
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
|
user?.twoFactorEnabled ? setDisableModalOpen(true) : setEnableModalOpen(true)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ const OnboardTeamMembersPage = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
|
OnboardTeamMembersPage.getLayout = (page: React.ReactElement) => (
|
||||||
<WizardLayout currentStep={1} maxSteps={2}>
|
<WizardLayout currentStep={2} maxSteps={2}>
|
||||||
{page}
|
{page}
|
||||||
</WizardLayout>
|
</WizardLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import Head from "next/head";
|
|||||||
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
import { getLayout } from "@components/layouts/WizardLayout";
|
import WizardLayout from "@components/layouts/WizardLayout";
|
||||||
|
|
||||||
const CreateNewTeamPage = () => {
|
const CreateNewTeamPage = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -17,7 +17,14 @@ const CreateNewTeamPage = () => {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
const LayoutWrapper = (page: React.ReactElement) => {
|
||||||
|
return (
|
||||||
|
<WizardLayout currentStep={1} maxSteps={2}>
|
||||||
|
{page}
|
||||||
|
</WizardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
CreateNewTeamPage.getLayout = getLayout;
|
CreateNewTeamPage.getLayout = LayoutWrapper;
|
||||||
|
|
||||||
export default CreateNewTeamPage;
|
export default CreateNewTeamPage;
|
||||||
@@ -10,7 +10,7 @@ import { CAL_URL } from "@calcom/lib/constants";
|
|||||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { md } from "@calcom/lib/markdownIt";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -113,7 +113,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
|||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
|
dangerouslySetInnerHTML={{ __html: team.safeBio }}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -187,11 +187,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
...user,
|
...user,
|
||||||
avatar: CAL_URL + "/" + user.username + "/avatar.png",
|
avatar: CAL_URL + "/" + user.username + "/avatar.png",
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const safeBio = markdownToSafeHTML(team.bio) || "";
|
||||||
|
|
||||||
|
const members = team.members.map((member) => {
|
||||||
|
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
team,
|
team: { ...team, safeBio, members },
|
||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { LocationObject } from "@calcom/core/location";
|
|||||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import { getWorkingHours } from "@calcom/lib/availability";
|
import { getWorkingHours } from "@calcom/lib/availability";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
@@ -63,7 +64,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
avatar: true,
|
|
||||||
username: true,
|
username: true,
|
||||||
timeZone: true,
|
timeZone: true,
|
||||||
hideBranding: true,
|
hideBranding: true,
|
||||||
@@ -170,6 +170,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
hideBranding,
|
hideBranding,
|
||||||
timeZone,
|
timeZone,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
});
|
});
|
||||||
|
|
||||||
eventTypeObject.availability = [];
|
eventTypeObject.availability = [];
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
|||||||
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
@@ -121,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
image: u.avatar,
|
image: u.avatar,
|
||||||
slug: u.username,
|
slug: u.username,
|
||||||
})),
|
})),
|
||||||
|
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||||
};
|
};
|
||||||
})[0];
|
})[0];
|
||||||
|
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ test.describe("Booking with Seats", () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(oldBooking?.attendees).toBeFalsy();
|
expect(oldBooking?.status).toBe(BookingStatus.CANCELLED);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Should cancel with seats and have no attendees and cancelled", async ({
|
test("Should cancel with seats and have no attendees and cancelled", async ({
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export const createEmbedsFixture = (page: Page) => {
|
|||||||
}
|
}
|
||||||
api("on", {
|
api("on", {
|
||||||
action: "*",
|
action: "*",
|
||||||
callback: (e: any) => {
|
callback: (e) => {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
|
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
|
||||||
|
|||||||
@@ -97,13 +97,19 @@ test.describe("Reschedule Tests", async () => {
|
|||||||
|
|
||||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||||
|
|
||||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } });
|
const newBooking = await prisma.booking.findFirstOrThrow({ where: { fromReschedule: booking.uid } });
|
||||||
const rescheduledBooking = await prisma.booking.findFirst({ where: { uid: booking.uid } });
|
const rescheduledBooking = await prisma.booking.findFirstOrThrow({ where: { uid: booking.uid } });
|
||||||
|
|
||||||
expect(newBooking).not.toBeNull();
|
expect(newBooking).not.toBeNull();
|
||||||
expect(rescheduledBooking).toBeNull();
|
expect(rescheduledBooking.status).toBe(BookingStatus.CANCELLED);
|
||||||
|
|
||||||
await prisma.booking.delete({ where: { id: newBooking?.id } });
|
await prisma.booking.deleteMany({
|
||||||
|
where: {
|
||||||
|
id: {
|
||||||
|
in: [newBooking.id, rescheduledBooking.id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => {
|
test("Unpaid rescheduling should go to payment page", async ({ page, users, bookings, payments }) => {
|
||||||
|
|||||||
@@ -5,3 +5,5 @@ Disallow: /static/locales
|
|||||||
|
|
||||||
# Specifically allow access to OG Image api, otherwise eg Twitter won't render these images.
|
# Specifically allow access to OG Image api, otherwise eg Twitter won't render these images.
|
||||||
Allow: /api/social/og/image
|
Allow: /api/social/og/image
|
||||||
|
# Specifically allow access to api/app-store to allow search engines to fetch assets
|
||||||
|
Allow: /api/app-store/
|
||||||
@@ -404,6 +404,7 @@
|
|||||||
"add_time_availability": "Add new time slot",
|
"add_time_availability": "Add new time slot",
|
||||||
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
|
"add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.",
|
||||||
"2fa": "Two-Factor Authentication",
|
"2fa": "Two-Factor Authentication",
|
||||||
|
"2fa_disabled":"Two-Factor authentication can only be enabled for email and password authentication",
|
||||||
"enable_2fa": "Enable two-factor authentication",
|
"enable_2fa": "Enable two-factor authentication",
|
||||||
"disable_2fa": "Disable two-factor authentication",
|
"disable_2fa": "Disable two-factor authentication",
|
||||||
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
|
"disable_2fa_recommendation": "If you need to disable 2FA, we recommend re-enabling it as soon as possible.",
|
||||||
@@ -1218,6 +1219,7 @@
|
|||||||
"impersonation": "Impersonation",
|
"impersonation": "Impersonation",
|
||||||
"impersonation_description": "Settings to manage user impersonation",
|
"impersonation_description": "Settings to manage user impersonation",
|
||||||
"users": "Users",
|
"users": "Users",
|
||||||
|
"user": "User",
|
||||||
"profile_description": "Manage settings for your {{appName}} profile",
|
"profile_description": "Manage settings for your {{appName}} profile",
|
||||||
"users_description": "Here you can find a list of all users",
|
"users_description": "Here you can find a list of all users",
|
||||||
"users_listing": "User listing",
|
"users_listing": "User listing",
|
||||||
@@ -1499,7 +1501,7 @@
|
|||||||
"date_overrides_update_btn": "Update Override",
|
"date_overrides_update_btn": "Update Override",
|
||||||
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
"event_type_duplicate_copy_text": "{{slug}}-copy",
|
||||||
"set_as_default": "Set as default",
|
"set_as_default": "Set as default",
|
||||||
"hide_eventtype_details": "Hide EventType Details",
|
"hide_eventtype_details": "Hide event type details",
|
||||||
"show_navigation": "Show navigation",
|
"show_navigation": "Show navigation",
|
||||||
"hide_navigation": "Hide navigation",
|
"hide_navigation": "Hide navigation",
|
||||||
"verification_code_sent": "Verification code sent",
|
"verification_code_sent": "Verification code sent",
|
||||||
@@ -1513,7 +1515,7 @@
|
|||||||
"create_your_first_team_webhook_description": "Create your first webhook for this team event type",
|
"create_your_first_team_webhook_description": "Create your first webhook for this team event type",
|
||||||
"create_webhook_team_event_type": "Create a webhook for this team event type",
|
"create_webhook_team_event_type": "Create a webhook for this team event type",
|
||||||
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
|
"disable_success_page": "Disable Success Page (only works if you have a redirect URL)",
|
||||||
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters",
|
"invalid_admin_password": "You are admin but you do not have a password length of at least 15 characters or no 2FA yet",
|
||||||
"change_password_admin": "Change Password to gain admin access",
|
"change_password_admin": "Change Password to gain admin access",
|
||||||
"username_already_taken": "Username is already taken",
|
"username_already_taken": "Username is already taken",
|
||||||
"assignment": "Assignment",
|
"assignment": "Assignment",
|
||||||
@@ -1608,6 +1610,7 @@
|
|||||||
"default_app_link_title": "Set a default app link",
|
"default_app_link_title": "Set a default app link",
|
||||||
"default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.",
|
"default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.",
|
||||||
"change_default_conferencing_app": "Set as default",
|
"change_default_conferencing_app": "Set as default",
|
||||||
|
"organizer_default_conferencing_app": "Organizer's default app",
|
||||||
"under_maintenance": "Down for maintenance",
|
"under_maintenance": "Down for maintenance",
|
||||||
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
|
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
|
||||||
"event_type_seats": "{{numberOfSeats}} seats",
|
"event_type_seats": "{{numberOfSeats}} seats",
|
||||||
@@ -1686,7 +1689,10 @@
|
|||||||
"events_rescheduled": "Events Rescheduled",
|
"events_rescheduled": "Events Rescheduled",
|
||||||
"from_last_period": "from last period",
|
"from_last_period": "from last period",
|
||||||
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
|
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
|
||||||
"analytics_for_organisation": "Analytics for {{organisationName}}",
|
"analytics_for_organisation": "Insights for {{organisationName}}",
|
||||||
"subtitle_analytics": "This is a organisation analytics",
|
"subtitle_analytics": "Learn more about your team's activity",
|
||||||
"event_trends": "Event Trends"
|
"event_trends": "Event Trends",
|
||||||
|
"clear_filters": "Clear Filters",
|
||||||
|
"insights": "Insights",
|
||||||
|
"email_invite_team": "{{email}} has been invited"
|
||||||
}
|
}
|
||||||
@@ -1213,6 +1213,7 @@
|
|||||||
"impersonation": "Suplantación",
|
"impersonation": "Suplantación",
|
||||||
"impersonation_description": "Configuración para administrar la suplantación del usuario",
|
"impersonation_description": "Configuración para administrar la suplantación del usuario",
|
||||||
"users": "Usuarios",
|
"users": "Usuarios",
|
||||||
|
"user": "Usuario",
|
||||||
"profile_description": "Administra los ajustes para tu perfil de {{appName}}",
|
"profile_description": "Administra los ajustes para tu perfil de {{appName}}",
|
||||||
"users_description": "Aquí puede encontrar una lista de todos los usuarios",
|
"users_description": "Aquí puede encontrar una lista de todos los usuarios",
|
||||||
"users_listing": "Lista de usuarios",
|
"users_listing": "Lista de usuarios",
|
||||||
|
|||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -497,3 +497,5 @@ hr {
|
|||||||
::-webkit-search-cancel-button {
|
::-webkit-search-cancel-button {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
+3
-1
@@ -70,6 +70,7 @@
|
|||||||
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"type-check": "turbo run type-check",
|
"type-check": "turbo run type-check",
|
||||||
|
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
||||||
"web": "yarn workspace @calcom/web"
|
"web": "yarn workspace @calcom/web"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -86,8 +87,9 @@
|
|||||||
"jest-watch-select-projects": "^2.0.0",
|
"jest-watch-select-projects": "^2.0.0",
|
||||||
"jest-watch-typeahead": "^2.0.0",
|
"jest-watch-typeahead": "^2.0.0",
|
||||||
"lint-staged": "^12.5.0",
|
"lint-staged": "^12.5.0",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.6",
|
||||||
"ts-jest": "^28.0.8",
|
"ts-jest": "^28.0.8",
|
||||||
|
"tsc-absolute": "^1.0.0",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
|
||||||
import seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
|
import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
|
||||||
|
|
||||||
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
|
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
|
||||||
import execSync from "./utils/execSync";
|
import execSync from "./utils/execSync";
|
||||||
@@ -127,6 +127,8 @@ export const BaseAppFork = {
|
|||||||
.replace(/_DESCRIPTION_/g, description)
|
.replace(/_DESCRIPTION_/g, description)
|
||||||
.replace(/_APP_DIR_/g, slug)
|
.replace(/_APP_DIR_/g, slug)
|
||||||
);
|
);
|
||||||
|
// New monorepo package has been added, so we need to run yarn again
|
||||||
|
await execSync("yarn");
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) {
|
delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ items:
|
|||||||
{DESCRIPTION}
|
{DESCRIPTION}
|
||||||
|
|
||||||
### Cron's most valuable features
|
### Cron's most valuable features
|
||||||
|
|
||||||
<!-- From "FAQs: What are Cron’s most valuable features?"
|
<!-- From "FAQs: What are Cron’s most valuable features?"
|
||||||
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
|
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
|
||||||
|
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
type: "google_calendar",
|
type: "google_calendar",
|
||||||
password: "",
|
password: "",
|
||||||
url: "",
|
url: "",
|
||||||
|
iCalUID: event.data.iCalUID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -201,6 +202,9 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
id: String(event.organizer.id),
|
id: String(event.organizer.id),
|
||||||
organizer: true,
|
organizer: true,
|
||||||
responseStatus: "accepted",
|
responseStatus: "accepted",
|
||||||
|
email: event.destinationCalendar?.externalId
|
||||||
|
? event.destinationCalendar.externalId
|
||||||
|
: event.organizer.email,
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
...eventAttendees,
|
...eventAttendees,
|
||||||
@@ -268,6 +272,7 @@ export default class GoogleCalendarService implements Calendar {
|
|||||||
type: "google_calendar",
|
type: "google_calendar",
|
||||||
password: "",
|
password: "",
|
||||||
url: "",
|
url: "",
|
||||||
|
iCalUID: evt.data.iCalUID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return resolve(evt?.data);
|
return resolve(evt?.data);
|
||||||
|
|||||||
@@ -12,13 +12,20 @@ export type DefaultEventLocationType = {
|
|||||||
type: DefaultEventLocationTypeEnum;
|
type: DefaultEventLocationTypeEnum;
|
||||||
label: string;
|
label: string;
|
||||||
messageForOrganizer: string;
|
messageForOrganizer: string;
|
||||||
category: "in person" | "other" | "phone";
|
category: "in person" | "conferencing" | "other" | "phone";
|
||||||
|
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
urlRegExp?: string;
|
urlRegExp?: string;
|
||||||
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
|
// HACK: `variable` and `defaultValueVariable` are required due to legacy reason where different locations were stored in different places.
|
||||||
variable: "locationType" | "locationAddress" | "address" | "locationLink" | "locationPhoneNumber" | "phone";
|
variable:
|
||||||
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone";
|
| "locationType"
|
||||||
|
| "locationAddress"
|
||||||
|
| "address"
|
||||||
|
| "locationLink"
|
||||||
|
| "locationPhoneNumber"
|
||||||
|
| "phone"
|
||||||
|
| "hostDefault";
|
||||||
|
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone";
|
||||||
} & (
|
} & (
|
||||||
| {
|
| {
|
||||||
organizerInputType: "phone" | "text" | null;
|
organizerInputType: "phone" | "text" | null;
|
||||||
@@ -60,6 +67,7 @@ export enum DefaultEventLocationTypeEnum {
|
|||||||
*/
|
*/
|
||||||
UserPhone = "userPhone",
|
UserPhone = "userPhone",
|
||||||
Link = "link",
|
Link = "link",
|
||||||
|
Conferencing = "conferencing",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultLocations: DefaultEventLocationType[] = [
|
export const defaultLocations: DefaultEventLocationType[] = [
|
||||||
@@ -88,6 +96,17 @@ export const defaultLocations: DefaultEventLocationType[] = [
|
|||||||
iconUrl: "/map-pin.svg",
|
iconUrl: "/map-pin.svg",
|
||||||
category: "in person",
|
category: "in person",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
default: true,
|
||||||
|
type: DefaultEventLocationTypeEnum.Conferencing,
|
||||||
|
iconUrl: "/link.svg",
|
||||||
|
organizerInputType: null,
|
||||||
|
label: "organizer_default_conferencing_app",
|
||||||
|
variable: "hostDefault",
|
||||||
|
defaultValueVariable: "hostDefault",
|
||||||
|
category: "conferencing",
|
||||||
|
messageForOrganizer: "",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
default: true,
|
default: true,
|
||||||
type: DefaultEventLocationTypeEnum.Link,
|
type: DefaultEventLocationTypeEnum.Link,
|
||||||
@@ -130,7 +149,9 @@ export const defaultLocations: DefaultEventLocationType[] = [
|
|||||||
export type LocationObject = {
|
export type LocationObject = {
|
||||||
type: string;
|
type: string;
|
||||||
displayLocationPublicly?: boolean;
|
displayLocationPublicly?: boolean;
|
||||||
} & Partial<Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone", string>>;
|
} & Partial<
|
||||||
|
Record<"address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone", string>
|
||||||
|
>;
|
||||||
|
|
||||||
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
|
// integrations:jitsi | 919999999999 | Delhi | https://manual.meeting.link | Around Video
|
||||||
export type BookingLocationValue = string;
|
export type BookingLocationValue = string;
|
||||||
|
|||||||
@@ -73,7 +73,9 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
body: JSON.stringify(this.translateEvent(event)),
|
body: JSON.stringify(this.translateEvent(event)),
|
||||||
});
|
});
|
||||||
|
|
||||||
return handleErrorsJson(response);
|
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
|
||||||
|
|
||||||
|
return { ...responseJson, iCalUID: responseJson.iCalUId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.error(error);
|
this.log.error(error);
|
||||||
|
|
||||||
@@ -88,7 +90,9 @@ export default class Office365CalendarService implements Calendar {
|
|||||||
body: JSON.stringify(this.translateEvent(event)),
|
body: JSON.stringify(this.translateEvent(event)),
|
||||||
});
|
});
|
||||||
|
|
||||||
return handleErrorsRaw(response);
|
const responseJson = await handleErrorsJson<NewCalendarEventType & { iCalUId: string }>(response);
|
||||||
|
|
||||||
|
return { ...responseJson, iCalUID: responseJson.iCalUId };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.log.error(error);
|
this.log.error(error);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||||
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
||||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||||
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
|
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
|
||||||
|
|||||||
+3
-2
@@ -4,6 +4,8 @@ import type { Settings, Widgets, SelectWidgetProps } from "react-awesome-query-b
|
|||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
import BasicConfig from "react-awesome-query-builder/lib/config/basic";
|
import BasicConfig from "react-awesome-query-builder/lib/config/basic";
|
||||||
|
|
||||||
|
import { EmailField } from "@calcom/ui";
|
||||||
|
|
||||||
import widgetsComponents from "../widgets";
|
import widgetsComponents from "../widgets";
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -92,8 +94,7 @@ const widgets: Widgets & { [key in keyof Widgets]: Widgets[key] & { type: string
|
|||||||
if (!props) {
|
if (!props) {
|
||||||
return <div />;
|
return <div />;
|
||||||
}
|
}
|
||||||
// TODO: Use EmailField component for Routing Form Email field
|
return <EmailField type="email" {...props} />;
|
||||||
return <TextWidget type="email" {...props} />;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export default function RoutingForms({
|
|||||||
title={t("teams_plan_required")}
|
title={t("teams_plan_required")}
|
||||||
description={t("routing_forms_are_a_great_way")}
|
description={t("routing_forms_are_a_great_way")}
|
||||||
features={features}
|
features={features}
|
||||||
background="/banners/routing-forms.jpg"
|
background="/tips/routing-forms.jpg"
|
||||||
isParentLoading={isLoading && <SkeletonLoaderTeamList />}
|
isParentLoading={isLoading && <SkeletonLoaderTeamList />}
|
||||||
buttons={
|
buttons={
|
||||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||||
|
|||||||
Whitespace-only changes.
@@ -1,14 +0,0 @@
|
|||||||
/* @see https://github.com/tailwindlabs/prettier-plugin-tailwindcss/issues/31#issuecomment-1024722576 */
|
|
||||||
const tailwind = require("prettier-plugin-tailwindcss");
|
|
||||||
const sortImports = require("@trivago/prettier-plugin-sort-imports");
|
|
||||||
|
|
||||||
const combinedFormatter = {
|
|
||||||
parsers: {
|
|
||||||
typescript: {
|
|
||||||
...tailwind.parsers.typescript,
|
|
||||||
preprocess: sortImports.parsers.typescript.preprocess,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = combinedFormatter;
|
|
||||||
@@ -20,13 +20,13 @@
|
|||||||
"@tailwindcss/forms": "^0.5.2",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
"@tailwindcss/line-clamp": "^0.4.0",
|
"@tailwindcss/line-clamp": "^0.4.0",
|
||||||
"@tailwindcss/typography": "^0.5.4",
|
"@tailwindcss/typography": "^0.5.4",
|
||||||
"@trivago/prettier-plugin-sort-imports": "3.2.0",
|
"@trivago/prettier-plugin-sort-imports": "4.1.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
"@typescript-eslint/eslint-plugin": "^5.52.0",
|
||||||
"@typescript-eslint/parser": "^5.52.0",
|
"@typescript-eslint/parser": "^5.52.0",
|
||||||
"eslint": "^8.34.0",
|
"eslint": "^8.34.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"prettier": "^2.8.4",
|
"prettier": "^2.8.6",
|
||||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
"prettier-plugin-tailwindcss": "^0.2.5",
|
||||||
"tailwind-scrollbar": "^2.0.1",
|
"tailwind-scrollbar": "^2.0.1",
|
||||||
"tailwindcss": "^3.2.1",
|
"tailwindcss": "^3.2.1",
|
||||||
"typescript": "^4.9.4"
|
"typescript": "^4.9.4"
|
||||||
|
|||||||
@@ -16,7 +16,14 @@ module.exports = {
|
|||||||
"^[./]",
|
"^[./]",
|
||||||
],
|
],
|
||||||
importOrderSeparation: true,
|
importOrderSeparation: true,
|
||||||
plugins: [require("./merged-prettier-plugin")],
|
plugins: [
|
||||||
|
"@trivago/prettier-plugin-sort-imports",
|
||||||
|
/**
|
||||||
|
* **NOTE** tailwind plugin must come last!
|
||||||
|
* @see https://github.com/tailwindlabs/prettier-plugin-tailwindcss#compatibility-with-other-prettier-plugins
|
||||||
|
*/
|
||||||
|
"prettier-plugin-tailwindcss",
|
||||||
|
],
|
||||||
overrides: [
|
overrides: [
|
||||||
{
|
{
|
||||||
files: ["apps/website/lib/utils/wordlist/wordlist.ts"],
|
files: ["apps/website/lib/utils/wordlist/wordlist.ts"],
|
||||||
|
|||||||
@@ -77,9 +77,11 @@ export const getConnectedCalendars = async (
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (destinationCalendar) {
|
// HACK https://github.com/calcom/cal.com/pull/7644/files#r1131508414
|
||||||
|
if (destinationCalendar && !Object.isFrozen(destinationCalendar)) {
|
||||||
destinationCalendar.primaryEmail = primary.email;
|
destinationCalendar.primaryEmail = primary.email;
|
||||||
destinationCalendar.integrationTitle = integration.title;
|
destinationCalendar.integrationTitle = integration.title;
|
||||||
|
destinationCalendar = Object.freeze(destinationCalendar);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -263,6 +265,7 @@ export const createEvent = async (
|
|||||||
type: credential.type,
|
type: credential.type,
|
||||||
success,
|
success,
|
||||||
uid,
|
uid,
|
||||||
|
iCalUID: creationResult?.iCalUID || undefined,
|
||||||
createdEvent: creationResult,
|
createdEvent: creationResult,
|
||||||
originalEvent: calEvent,
|
originalEvent: calEvent,
|
||||||
calError,
|
calError,
|
||||||
|
|||||||
@@ -10,15 +10,12 @@ import { MeetLocationType } from "@calcom/app-store/locations";
|
|||||||
import getApps from "@calcom/app-store/utils";
|
import getApps from "@calcom/app-store/utils";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { createdEventSchema } from "@calcom/prisma/zod-utils";
|
import { createdEventSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { AdditionalInformation, CalendarEvent, NewCalendarEventType } from "@calcom/types/Calendar";
|
import type { NewCalendarEventType } from "@calcom/types/Calendar";
|
||||||
|
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||||
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
|
import type { CredentialPayload, CredentialWithAppName } from "@calcom/types/Credential";
|
||||||
import type { Event } from "@calcom/types/Event";
|
import type { Event } from "@calcom/types/Event";
|
||||||
import type {
|
import type { EventResult } from "@calcom/types/EventManager";
|
||||||
CreateUpdateResult,
|
import type { CreateUpdateResult, PartialBooking, PartialReference } from "@calcom/types/EventManager";
|
||||||
EventResult,
|
|
||||||
PartialBooking,
|
|
||||||
PartialReference,
|
|
||||||
} from "@calcom/types/EventManager";
|
|
||||||
|
|
||||||
import { createEvent, updateEvent } from "./CalendarManager";
|
import { createEvent, updateEvent } from "./CalendarManager";
|
||||||
import { createMeeting, updateMeeting } from "./videoClient";
|
import { createMeeting, updateMeeting } from "./videoClient";
|
||||||
@@ -125,12 +122,24 @@ export default class EventManager {
|
|||||||
// Create the calendar event with the proper video call data
|
// Create the calendar event with the proper video call data
|
||||||
results.push(...(await this.createAllCalendarEvents(clonedCalEvent)));
|
results.push(...(await this.createAllCalendarEvents(clonedCalEvent)));
|
||||||
|
|
||||||
|
// Since the result can be a new calendar event or video event, we have to create a type guard
|
||||||
|
// https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
|
||||||
|
const isCalendarResult = (
|
||||||
|
result: (typeof results)[number]
|
||||||
|
): result is EventResult<NewCalendarEventType> => {
|
||||||
|
return result.type.includes("_calendar");
|
||||||
|
};
|
||||||
|
|
||||||
const referencesToCreate = results.map((result) => {
|
const referencesToCreate = results.map((result) => {
|
||||||
let createdEventObj: createdEventSchema | null = null;
|
let createdEventObj: createdEventSchema | null = null;
|
||||||
if (typeof result?.createdEvent === "string") {
|
if (typeof result?.createdEvent === "string") {
|
||||||
createdEventObj = createdEventSchema.parse(JSON.parse(result.createdEvent));
|
createdEventObj = createdEventSchema.parse(JSON.parse(result.createdEvent));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isCalendarResult(result)) {
|
||||||
|
evt.iCalUID = result.iCalUID || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: result.type,
|
type: result.type,
|
||||||
uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "",
|
uid: createdEventObj ? createdEventObj.id : result.createdEvent?.id?.toString() ?? "",
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
|
|
||||||
const Spacer = () => <p style={{ height: 6 }} />;
|
const Spacer = () => <p style={{ height: 6 }} />;
|
||||||
|
|
||||||
export const Info = (props: {
|
export const Info = (props: {
|
||||||
@@ -6,8 +8,14 @@ export const Info = (props: {
|
|||||||
extraInfo?: React.ReactNode;
|
extraInfo?: React.ReactNode;
|
||||||
withSpacer?: boolean;
|
withSpacer?: boolean;
|
||||||
lineThrough?: boolean;
|
lineThrough?: boolean;
|
||||||
|
formatted?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
if (!props.description || props.description === "") return null;
|
if (!props.description || props.description === "") return null;
|
||||||
|
|
||||||
|
const descriptionCSS = "color: '#101010'; font-weight: 400; line-height: 24px; margin: 0;";
|
||||||
|
|
||||||
|
const safeDescription = markdownToSafeHTML(props.description.toString()) || "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{props.withSpacer && <Spacer />}
|
{props.withSpacer && <Spacer />}
|
||||||
@@ -21,7 +29,18 @@ export const Info = (props: {
|
|||||||
whiteSpace: "pre-wrap",
|
whiteSpace: "pre-wrap",
|
||||||
textDecoration: props.lineThrough ? "line-through" : undefined,
|
textDecoration: props.lineThrough ? "line-through" : undefined,
|
||||||
}}>
|
}}>
|
||||||
{props.description}
|
{props.formatted ? (
|
||||||
|
<p
|
||||||
|
className="dark:text-darkgray-600 mt-2 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: safeDescription
|
||||||
|
.replaceAll("<p>", `<p style="${descriptionCSS}">`)
|
||||||
|
.replaceAll("<li>", `<li style="${descriptionCSS}">`),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
props.description
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
{props.extraInfo}
|
{props.extraInfo}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ export const BaseScheduledEmail = (
|
|||||||
<WhenInfo calEvent={props.calEvent} t={t} timeZone={timeZone} />
|
<WhenInfo calEvent={props.calEvent} t={t} timeZone={timeZone} />
|
||||||
<WhoInfo calEvent={props.calEvent} t={t} />
|
<WhoInfo calEvent={props.calEvent} t={t} />
|
||||||
<LocationInfo calEvent={props.calEvent} t={t} />
|
<LocationInfo calEvent={props.calEvent} t={t} />
|
||||||
<Info label={t("description")} description={props.calEvent.description} withSpacer />
|
<Info label={t("description")} description={props.calEvent.description} withSpacer formatted />
|
||||||
<Info label={t("additional_notes")} description={props.calEvent.additionalNotes} withSpacer />
|
<Info label={t("additional_notes")} description={props.calEvent.additionalNotes} withSpacer />
|
||||||
{props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />}
|
{props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />}
|
||||||
<UserFieldsResponses calEvent={props.calEvent} />
|
<UserFieldsResponses calEvent={props.calEvent} />
|
||||||
|
|||||||
@@ -16,20 +16,18 @@ export default class AttendeeScheduledEmail extends BaseEmail {
|
|||||||
attendee: Person;
|
attendee: Person;
|
||||||
showAttendees: boolean | undefined;
|
showAttendees: boolean | undefined;
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
attendees: Person[];
|
|
||||||
|
|
||||||
constructor(calEvent: CalendarEvent, attendee: Person, showAttendees?: boolean | undefined) {
|
constructor(calEvent: CalendarEvent, attendee: Person, showAttendees?: boolean | undefined) {
|
||||||
super();
|
super();
|
||||||
this.name = "SEND_BOOKING_CONFIRMATION";
|
if (!showAttendees && calEvent.seatsPerTimeSlot) {
|
||||||
|
this.calEvent = cloneDeep(calEvent);
|
||||||
|
this.calEvent.attendees = [attendee];
|
||||||
|
} else {
|
||||||
this.calEvent = calEvent;
|
this.calEvent = calEvent;
|
||||||
this.attendee = attendee;
|
|
||||||
this.showAttendees = showAttendees;
|
|
||||||
this.t = attendee.language.translate;
|
|
||||||
this.attendees = [...this.calEvent.attendees];
|
|
||||||
if (!this.showAttendees && this.calEvent.seatsPerTimeSlot) {
|
|
||||||
this.attendees = [this.attendee];
|
|
||||||
this.calEvent.attendees = [this.attendee];
|
|
||||||
}
|
}
|
||||||
|
this.name = "SEND_BOOKING_CONFIRMATION";
|
||||||
|
this.attendee = attendee;
|
||||||
|
this.t = attendee.language.translate;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected getiCalEventAsString(): string | undefined {
|
protected getiCalEventAsString(): string | undefined {
|
||||||
@@ -39,21 +37,21 @@ export default class AttendeeScheduledEmail extends BaseEmail {
|
|||||||
// ics appends "RRULE:" already, so removing it from RRule generated string
|
// ics appends "RRULE:" already, so removing it from RRule generated string
|
||||||
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
|
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
|
||||||
}
|
}
|
||||||
const partstat: ParticipationStatus = "NEEDS-ACTION";
|
const partstat: ParticipationStatus = "ACCEPTED";
|
||||||
const role: ParticipationRole = "REQ-PARTICIPANT";
|
const role: ParticipationRole = "REQ-PARTICIPANT";
|
||||||
const icsEvent = createEvent({
|
const icsEvent = createEvent({
|
||||||
|
uid: this.calEvent.iCalUID || this.calEvent.uid!,
|
||||||
start: dayjs(this.calEvent.startTime)
|
start: dayjs(this.calEvent.startTime)
|
||||||
.utc()
|
.utc()
|
||||||
.toArray()
|
.toArray()
|
||||||
.slice(0, 6)
|
.slice(0, 6)
|
||||||
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
|
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
|
||||||
startInputType: "utc",
|
startInputType: "utc",
|
||||||
productId: "calendso/ics",
|
productId: "calcom/ics",
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
description: this.getTextBody(),
|
description: this.getTextBody(),
|
||||||
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
|
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
|
||||||
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
|
organizer: { name: this.calEvent.organizer.name, email: this.calEvent.organizer.email },
|
||||||
|
|
||||||
attendees: [
|
attendees: [
|
||||||
...this.calEvent.attendees.map((attendee: Person) => ({
|
...this.calEvent.attendees.map((attendee: Person) => ({
|
||||||
name: attendee.name,
|
name: attendee.name,
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default class AttendeeWasRequestedToRescheduleEmail extends OrganizerSche
|
|||||||
.slice(0, 6)
|
.slice(0, 6)
|
||||||
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
|
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
|
||||||
startInputType: "utc",
|
startInputType: "utc",
|
||||||
productId: "calendso/ics",
|
productId: "calcom/ics",
|
||||||
title: this.t("ics_event_title", {
|
title: this.t("ics_event_title", {
|
||||||
eventType: this.calEvent.type,
|
eventType: this.calEvent.type,
|
||||||
name: this.calEvent.attendees[0].name,
|
name: this.calEvent.attendees[0].name,
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export default class OrganizerRequestedToRescheduleEmail extends OrganizerSchedu
|
|||||||
.slice(0, 6)
|
.slice(0, 6)
|
||||||
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
|
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
|
||||||
startInputType: "utc",
|
startInputType: "utc",
|
||||||
productId: "calendso/ics",
|
productId: "calcom/ics",
|
||||||
title: this.t("ics_event_title", {
|
title: this.t("ics_event_title", {
|
||||||
eventType: this.calEvent.type,
|
eventType: this.calEvent.type,
|
||||||
name: this.calEvent.attendees[0].name,
|
name: this.calEvent.attendees[0].name,
|
||||||
|
|||||||
@@ -35,13 +35,14 @@ export default class OrganizerScheduledEmail extends BaseEmail {
|
|||||||
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
|
recurrenceRule = new RRule(this.calEvent.recurringEvent).toString().replace("RRULE:", "");
|
||||||
}
|
}
|
||||||
const icsEvent = createEvent({
|
const icsEvent = createEvent({
|
||||||
|
uid: this.calEvent.iCalUID || this.calEvent.uid!,
|
||||||
start: dayjs(this.calEvent.startTime)
|
start: dayjs(this.calEvent.startTime)
|
||||||
.utc()
|
.utc()
|
||||||
.toArray()
|
.toArray()
|
||||||
.slice(0, 6)
|
.slice(0, 6)
|
||||||
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
|
.map((v, i) => (i === 1 ? v + 1 : v)) as DateArray,
|
||||||
startInputType: "utc",
|
startInputType: "utc",
|
||||||
productId: "calendso/ics",
|
productId: "calcom/ics",
|
||||||
title: this.calEvent.title,
|
title: this.calEvent.title,
|
||||||
description: this.getTextBody(),
|
description: this.getTextBody(),
|
||||||
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
|
duration: { minutes: dayjs(this.calEvent.endTime).diff(dayjs(this.calEvent.startTime), "minute") },
|
||||||
|
|||||||
@@ -4,6 +4,16 @@
|
|||||||
<!-- <link rel="prerender" href="http://localhost:3000/free"> -->
|
<!-- <link rel="prerender" href="http://localhost:3000/free"> -->
|
||||||
<!-- <script src="./src/embed.ts" type="module"></script> -->
|
<!-- <script src="./src/embed.ts" type="module"></script> -->
|
||||||
<script>
|
<script>
|
||||||
|
function generateRandomHexColor() {
|
||||||
|
// Generate a random integer between 0 and 16777215 (FFFFFF in hex)
|
||||||
|
const randomInt = Math.floor(Math.random() * 16777216);
|
||||||
|
|
||||||
|
// Convert the integer to a hex string with 6 digits and add leading zeros if necessary
|
||||||
|
const hexString = randomInt.toString(16).padStart(6, '0');
|
||||||
|
|
||||||
|
// Return the hex string with a '#' prefix
|
||||||
|
return `#${hexString}`;
|
||||||
|
}
|
||||||
if (!location.search.includes("nonResponsive")) {
|
if (!location.search.includes("nonResponsive")) {
|
||||||
document.write('<meta name="viewport" content="width=device-width"/>');
|
document.write('<meta name="viewport" content="width=device-width"/>');
|
||||||
}
|
}
|
||||||
@@ -90,6 +100,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<span style="display: block;" ><a href="?only=all">Render All embeds together</a><small> - It would be slow to load</small></span>
|
<span style="display: block;" ><a href="?only=all">Render All embeds together</a><small> - It would be slow to load</small></span>
|
||||||
<div>
|
<div>
|
||||||
|
<a href="?only=ns:floatingButton">Floating Popup</a>
|
||||||
|
|
||||||
<h2>Popup Examples</h2>
|
<h2>Popup Examples</h2>
|
||||||
<button data-cal-namespace="popupAutoTheme" data-cal-link="free">Book with Free User[Auto Theme]</button>
|
<button data-cal-namespace="popupAutoTheme" data-cal-link="free">Book with Free User[Auto Theme]</button>
|
||||||
@@ -140,12 +151,12 @@
|
|||||||
Align Center
|
Align Center
|
||||||
</button>
|
</button>
|
||||||
<button onclick="(function () {Cal.ns.second('ui', {styles:{enabledDateButton: {
|
<button onclick="(function () {Cal.ns.second('ui', {styles:{enabledDateButton: {
|
||||||
backgroundColor: '#D3D3D3',
|
backgroundColor: generateRandomHexColor(),
|
||||||
},
|
},
|
||||||
disabledDateButton: {
|
disabledDateButton: {
|
||||||
backgroundColor: 'lightslategray',
|
backgroundColor: generateRandomHexColor(),
|
||||||
},}})})()">
|
},}})})()">
|
||||||
Change Date Button Color
|
Change 'enabledDateButton` and `disabledDateButton` background Color
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@@ -189,318 +200,6 @@
|
|||||||
<div class="place">
|
<div class="place">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script src="./playground.ts"></script>
|
||||||
|
|
||||||
<script>
|
|
||||||
const callback = function (e) {
|
|
||||||
const detail = e.detail;
|
|
||||||
const namespace = detail.namespace || "default";
|
|
||||||
|
|
||||||
if (detail.type === "linkReady") {
|
|
||||||
document.querySelector(`#cal-booking-place-${namespace} .loader`).remove();
|
|
||||||
} else if (detail.type === "linkFailed") {
|
|
||||||
document.querySelector(`#cal-booking-place-${namespace} .loader`).remove();
|
|
||||||
}
|
|
||||||
document.querySelector(`#cal-booking-place-${namespace} .last-action`).innerHTML = JSON.stringify(
|
|
||||||
e.detail
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const searchParams = new URL(document.URL).searchParams;
|
|
||||||
let only = window.only
|
|
||||||
if (only === "all" || only === "ns:default") {
|
|
||||||
Cal("init", {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
});
|
|
||||||
|
|
||||||
Cal("inline", {
|
|
||||||
elementOrSelector: "#cal-booking-place-default .place",
|
|
||||||
calLink: "pro?case=1",
|
|
||||||
config: {
|
|
||||||
__autoScroll:true,
|
|
||||||
iframeAttrs: {
|
|
||||||
id: "cal-booking-place-default-iframe"
|
|
||||||
},
|
|
||||||
name: "John",
|
|
||||||
email: "johndoe@gmail.com",
|
|
||||||
notes: "Test Meeting",
|
|
||||||
guests: ["janedoe@example.com", "test@example.com"],
|
|
||||||
theme: "dark",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Cal("on", {
|
|
||||||
action: "*",
|
|
||||||
callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (only === "all" || only === "ns:second") {
|
|
||||||
// Create a namespace "second". It can be accessed as Cal.ns.second with the exact same API as Cal
|
|
||||||
Cal("init", "second", {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
});
|
|
||||||
|
|
||||||
// Bulk API is supported - Keep all configuration at one place.
|
|
||||||
Cal.ns.second(
|
|
||||||
[
|
|
||||||
"inline",
|
|
||||||
{
|
|
||||||
elementOrSelector: "#cal-booking-place-second .place",
|
|
||||||
calLink: "pro?case=2",
|
|
||||||
config: {
|
|
||||||
iframeAttrs: {
|
|
||||||
id: "cal-booking-place-second-iframe"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
Cal.ns.second("on", {
|
|
||||||
action: "*",
|
|
||||||
callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (only === "all" || only === "ns:third") {
|
|
||||||
// Create a namespace "third". It can be accessed as Cal.ns.second with the exact same API as Cal
|
|
||||||
Cal("init", "third", {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
});
|
|
||||||
|
|
||||||
Cal.ns.third(
|
|
||||||
[
|
|
||||||
"inline",
|
|
||||||
{
|
|
||||||
elementOrSelector: "#cal-booking-place-third .place",
|
|
||||||
calLink: "pro/30min",
|
|
||||||
config: {
|
|
||||||
iframeAttrs: {
|
|
||||||
id: "cal-booking-place-third-iframe"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"ui",
|
|
||||||
{
|
|
||||||
styles: {
|
|
||||||
body: {
|
|
||||||
background: "transparent",
|
|
||||||
},
|
|
||||||
branding: {
|
|
||||||
brandColor: "#81e61c",
|
|
||||||
lightColor: "#494545",
|
|
||||||
lighterColor: "#4c4848",
|
|
||||||
lightestColor: "#7c7777",
|
|
||||||
highlightColor: "#9b0e0e",
|
|
||||||
medianColor: "black",
|
|
||||||
},
|
|
||||||
enabledDateButton: {
|
|
||||||
backgroundColor: "red"
|
|
||||||
},
|
|
||||||
disabledDateButton: {
|
|
||||||
backgroundColor: "green"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
Cal.ns.third("on", {
|
|
||||||
action: "*",
|
|
||||||
callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (only === "all" || only === "ns:fourth") {
|
|
||||||
Cal("init", "fourth", {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
});
|
|
||||||
Cal.ns.fourth(
|
|
||||||
[
|
|
||||||
"inline",
|
|
||||||
{
|
|
||||||
elementOrSelector: "#cal-booking-place-fourth .place",
|
|
||||||
calLink: "team/seeded-team",
|
|
||||||
config: {
|
|
||||||
iframeAttrs: {
|
|
||||||
id: "cal-booking-place-fourth-iframe"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"ui",
|
|
||||||
{
|
|
||||||
styles: {
|
|
||||||
body: {
|
|
||||||
background: "transparent",
|
|
||||||
},
|
|
||||||
branding: {
|
|
||||||
brandColor: "#81e61c",
|
|
||||||
lightColor: "#494545",
|
|
||||||
lighterColor: "#4c4848",
|
|
||||||
lightestColor: "#7c7777",
|
|
||||||
highlightColor: "#9b0e0e",
|
|
||||||
medianColor: "black",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
);
|
|
||||||
Cal.ns.fourth("on", {
|
|
||||||
action: "*",
|
|
||||||
callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (only === "all" || only === "ns:fifth") {
|
|
||||||
Cal("init", "fifth", {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
});
|
|
||||||
Cal.ns.fifth(
|
|
||||||
[
|
|
||||||
"inline",
|
|
||||||
{
|
|
||||||
elementOrSelector: "#cal-booking-place-fifth .place",
|
|
||||||
calLink: "team/seeded-team/collective-seeded-team-event",
|
|
||||||
config: {
|
|
||||||
iframeAttrs: {
|
|
||||||
id: "cal-booking-place-fifth-iframe"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
Cal.ns.fifth("on", {
|
|
||||||
action: "*",
|
|
||||||
callback,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (only === "all" || only === "prerender-test") {
|
|
||||||
Cal('init', 'prerendertestLightTheme', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
Cal.ns.prerendertestLightTheme("preload", {
|
|
||||||
calLink: "free",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (only === "all" || only === "inline-routing-form") {
|
|
||||||
Cal('init', 'inline-routing-form', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
Cal.ns['inline-routing-form'](
|
|
||||||
[
|
|
||||||
"inline",
|
|
||||||
{
|
|
||||||
elementOrSelector: "#cal-booking-place-inline-routing-form .place",
|
|
||||||
calLink: "forms/948ae412-d995-4865-875a-48302588de03",
|
|
||||||
config: {
|
|
||||||
iframeAttrs: {
|
|
||||||
id: "cal-booking-place-inline-routing-form-iframe"
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (only === "all" || only === "hideEventTypeDetails") {
|
|
||||||
let identifier = "hideEventTypeDetails"
|
|
||||||
Cal('init', identifier, {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
|
|
||||||
Cal.ns.hideEventTypeDetails([
|
|
||||||
['inline', {
|
|
||||||
elementOrSelector: `#cal-booking-place-${identifier} .place`,
|
|
||||||
calLink: "free/30min",
|
|
||||||
config: {
|
|
||||||
iframeAttrs: {
|
|
||||||
id: `cal-booking-place-${identifier}-iframe`
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}],
|
|
||||||
[
|
|
||||||
'ui', {
|
|
||||||
hideEventTypeDetails:true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
Cal('init', 'popupDarkTheme', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
|
|
||||||
Cal('init', 'popupHideEventTypeDetails', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
});
|
|
||||||
|
|
||||||
Cal.ns.popupHideEventTypeDetails('ui', {
|
|
||||||
hideEventTypeDetails:true
|
|
||||||
});
|
|
||||||
|
|
||||||
Cal('init', 'popupReschedule', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
|
|
||||||
Cal('init', 'popupAutoTheme', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
Cal('init', 'popupTeamLinkLightTheme', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
Cal('init', 'popupTeamLinkDarkTheme', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
|
|
||||||
Cal('init', 'popupTeamLinkDarkTheme', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
|
|
||||||
Cal('init', 'popupTeamLinksList', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
|
|
||||||
Cal('init', 'popupPaidEvent', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
|
|
||||||
Cal("init", "floatingButton", {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
});
|
|
||||||
|
|
||||||
Cal('init', 'routingFormAuto', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
|
|
||||||
Cal('init', 'routingFormDark', {
|
|
||||||
debug: 1,
|
|
||||||
origin: "http://localhost:3000",
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
if (only === "all" || only == "ns:floatingButton") {
|
|
||||||
Cal.ns.floatingButton("floatingButton", {
|
|
||||||
calLink: "pro"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script></script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
"dev": "yarn tailwind && run-p 'tailwind --watch' 'buildWatchAndServer'",
|
"dev": "yarn tailwind && run-p 'tailwind --watch' 'buildWatchAndServer'",
|
||||||
"dev-real": "vite dev --port 3100",
|
"dev-real": "vite dev --port 3100",
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||||
"lint": "eslint --ext .ts,.js src",
|
"lint": "eslint --ext .ts,.js src",
|
||||||
"lint:fix": "eslint --ext .ts,.js src --fix",
|
"lint:fix": "eslint --ext .ts,.js src --fix",
|
||||||
"embed-tests": "yarn playwright test --config=playwright/config/playwright.config.ts",
|
"embed-tests": "yarn playwright test --config=playwright/config/playwright.config.ts",
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
import type { GlobalCal, GlobalCalWithoutNs } from "./src/embed";
|
||||||
|
|
||||||
|
type A = GlobalCalWithoutNs;
|
||||||
|
const Cal = window.Cal as GlobalCal;
|
||||||
|
const callback = function (e) {
|
||||||
|
const detail = e.detail;
|
||||||
|
const namespace = detail.namespace || "default";
|
||||||
|
|
||||||
|
if (detail.type === "linkReady") {
|
||||||
|
document.querySelector(`#cal-booking-place-${namespace} .loader`).remove();
|
||||||
|
} else if (detail.type === "linkFailed") {
|
||||||
|
document.querySelector(`#cal-booking-place-${namespace} .loader`).remove();
|
||||||
|
}
|
||||||
|
document.querySelector(`#cal-booking-place-${namespace} .last-action`).innerHTML = JSON.stringify(e.detail);
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchParams = new URL(document.URL).searchParams;
|
||||||
|
const only = window.only;
|
||||||
|
|
||||||
|
if (only === "all" || only === "ns:default") {
|
||||||
|
Cal("init", {
|
||||||
|
debug: true,
|
||||||
|
calOrigin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("inline", {
|
||||||
|
elementOrSelector: "#cal-booking-place-default .place",
|
||||||
|
calLink: "pro?case=1",
|
||||||
|
config: {
|
||||||
|
iframeAttrs: {
|
||||||
|
id: "cal-booking-place-default-iframe",
|
||||||
|
},
|
||||||
|
name: "John",
|
||||||
|
email: "johndoe@gmail.com",
|
||||||
|
notes: "Test Meeting",
|
||||||
|
guests: ["janedoe@example.com", "test@example.com"],
|
||||||
|
theme: "dark",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
Cal("on", {
|
||||||
|
action: "*",
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (only === "all" || only === "ns:second") {
|
||||||
|
// Create a namespace "second". It can be accessed as Cal.ns.second with the exact same API as Cal
|
||||||
|
Cal("init", "second", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
// Bulk API is supported - Keep all configuration at one place.
|
||||||
|
// Not able to type Bulk Api correctly when the first argument itself is an array.
|
||||||
|
Cal.ns.second([
|
||||||
|
"inline",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
{
|
||||||
|
elementOrSelector: "#cal-booking-place-second .place",
|
||||||
|
calLink: "pro?case=2",
|
||||||
|
config: {
|
||||||
|
iframeAttrs: {
|
||||||
|
id: "cal-booking-place-second-iframe",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
Cal.ns.second("on", {
|
||||||
|
action: "*",
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (only === "all" || only === "ns:third") {
|
||||||
|
// Create a namespace "third". It can be accessed as Cal.ns.second with the exact same API as Cal
|
||||||
|
Cal("init", "third", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal.ns.third(
|
||||||
|
[
|
||||||
|
"inline",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
{
|
||||||
|
elementOrSelector: "#cal-booking-place-third .place",
|
||||||
|
calLink: "pro/30min",
|
||||||
|
config: {
|
||||||
|
iframeAttrs: {
|
||||||
|
id: "cal-booking-place-third-iframe",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ui",
|
||||||
|
{
|
||||||
|
styles: {
|
||||||
|
body: {
|
||||||
|
background: "transparent",
|
||||||
|
},
|
||||||
|
branding: {
|
||||||
|
brandColor: "#81e61c",
|
||||||
|
lightColor: "#494545",
|
||||||
|
lighterColor: "#4c4848",
|
||||||
|
lightestColor: "#7c7777",
|
||||||
|
highlightColor: "#9b0e0e",
|
||||||
|
medianColor: "black",
|
||||||
|
},
|
||||||
|
enabledDateButton: {
|
||||||
|
backgroundColor: "red",
|
||||||
|
},
|
||||||
|
disabledDateButton: {
|
||||||
|
backgroundColor: "green",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
Cal.ns.third("on", {
|
||||||
|
action: "*",
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (only === "all" || only === "ns:fourth") {
|
||||||
|
Cal("init", "fourth", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
Cal.ns.fourth(
|
||||||
|
[
|
||||||
|
"inline",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
{
|
||||||
|
elementOrSelector: "#cal-booking-place-fourth .place",
|
||||||
|
calLink: "team/seeded-team",
|
||||||
|
config: {
|
||||||
|
iframeAttrs: {
|
||||||
|
id: "cal-booking-place-fourth-iframe",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ui",
|
||||||
|
{
|
||||||
|
styles: {
|
||||||
|
body: {
|
||||||
|
background: "transparent",
|
||||||
|
},
|
||||||
|
branding: {
|
||||||
|
brandColor: "#81e61c",
|
||||||
|
lightColor: "#494545",
|
||||||
|
lighterColor: "#4c4848",
|
||||||
|
lightestColor: "#7c7777",
|
||||||
|
highlightColor: "#9b0e0e",
|
||||||
|
medianColor: "black",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
Cal.ns.fourth("on", {
|
||||||
|
action: "*",
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (only === "all" || only === "ns:fifth") {
|
||||||
|
Cal("init", "fifth", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
Cal.ns.fifth([
|
||||||
|
"inline",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
{
|
||||||
|
elementOrSelector: "#cal-booking-place-fifth .place",
|
||||||
|
calLink: "team/seeded-team/collective-seeded-team-event",
|
||||||
|
config: {
|
||||||
|
iframeAttrs: {
|
||||||
|
id: "cal-booking-place-fifth-iframe",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
Cal.ns.fifth("on", {
|
||||||
|
action: "*",
|
||||||
|
callback,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (only === "all" || only === "prerender-test") {
|
||||||
|
Cal("init", "prerendertestLightTheme", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
Cal.ns.prerendertestLightTheme("preload", {
|
||||||
|
calLink: "free",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (only === "all" || only === "inline-routing-form") {
|
||||||
|
Cal("init", "inline-routing-form", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
Cal.ns["inline-routing-form"]([
|
||||||
|
"inline",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
{
|
||||||
|
elementOrSelector: "#cal-booking-place-inline-routing-form .place",
|
||||||
|
calLink: "forms/948ae412-d995-4865-875a-48302588de03",
|
||||||
|
config: {
|
||||||
|
iframeAttrs: {
|
||||||
|
id: "cal-booking-place-inline-routing-form-iframe",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (only === "all" || only === "hideEventTypeDetails") {
|
||||||
|
const identifier = "hideEventTypeDetails";
|
||||||
|
Cal("init", identifier, {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal.ns.hideEventTypeDetails(
|
||||||
|
[
|
||||||
|
"inline",
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
{
|
||||||
|
elementOrSelector: `#cal-booking-place-${identifier} .place`,
|
||||||
|
calLink: "free/30min",
|
||||||
|
config: {
|
||||||
|
iframeAttrs: {
|
||||||
|
id: `cal-booking-place-${identifier}-iframe`,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"ui",
|
||||||
|
{
|
||||||
|
hideEventTypeDetails: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Cal("init", "popupDarkTheme", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("init", "popupHideEventTypeDetails", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal.ns.popupHideEventTypeDetails("ui", {
|
||||||
|
hideEventTypeDetails: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("init", "popupReschedule", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("init", "popupAutoTheme", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
Cal("init", "popupTeamLinkLightTheme", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
Cal("init", "popupTeamLinkDarkTheme", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("init", "popupTeamLinkDarkTheme", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("init", "popupTeamLinksList", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("init", "popupPaidEvent", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("init", "floatingButton", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("init", "routingFormAuto", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
Cal("init", "routingFormDark", {
|
||||||
|
debug: true,
|
||||||
|
origin: "http://localhost:3000",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (only === "all" || only == "ns:floatingButton") {
|
||||||
|
Cal.ns.floatingButton("floatingButton", {
|
||||||
|
calLink: "pro",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,36 @@
|
|||||||
import getFloatingButtonHtml from "./FloatingButtonHtml";
|
import getFloatingButtonHtml from "./FloatingButtonHtml";
|
||||||
|
|
||||||
|
type ModalTargetDatasetProps = {
|
||||||
|
calLink: string;
|
||||||
|
calNamespace: string;
|
||||||
|
calOrigin: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CamelCase<T extends string> = T extends `${infer U}${infer V}` ? `${Uppercase<U>}${V}` : T;
|
||||||
|
|
||||||
|
type HyphenatedStringToCamelCase<S extends string> = S extends `${infer T}-${infer U}`
|
||||||
|
? `${T}${HyphenatedStringToCamelCase<CamelCase<U>>}`
|
||||||
|
: CamelCase<S>;
|
||||||
|
|
||||||
|
type HyphenatedDataStringToCamelCase<S extends string> = S extends `data-${infer U}`
|
||||||
|
? HyphenatedStringToCamelCase<U>
|
||||||
|
: S;
|
||||||
|
|
||||||
|
const dataAttributes = [
|
||||||
|
"data-button-text",
|
||||||
|
"data-hide-button-icon",
|
||||||
|
"data-button-position",
|
||||||
|
"data-button-color",
|
||||||
|
"data-button-text-color",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
type DataAttributes = (typeof dataAttributes)[number];
|
||||||
|
type DataAttributesCamelCase = HyphenatedDataStringToCamelCase<DataAttributes>;
|
||||||
|
|
||||||
|
export type FloatingButtonDataset = {
|
||||||
|
[key in DataAttributesCamelCase]: string;
|
||||||
|
};
|
||||||
|
|
||||||
export class FloatingButton extends HTMLElement {
|
export class FloatingButton extends HTMLElement {
|
||||||
static updatedClassString(position: string, classString: string) {
|
static updatedClassString(position: string, classString: string) {
|
||||||
return [
|
return [
|
||||||
@@ -8,66 +39,69 @@ export class FloatingButton extends HTMLElement {
|
|||||||
].join(" ");
|
].join(" ");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Button added here triggers the modal on click. So, it has to have the same data attributes as the modal target as well
|
||||||
|
dataset!: DOMStringMap & FloatingButtonDataset & ModalTargetDatasetProps;
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return [
|
return dataAttributes;
|
||||||
"data-button-text",
|
|
||||||
"data-hide-button-icon",
|
|
||||||
"data-button-position",
|
|
||||||
"data-button-color",
|
|
||||||
"data-button-text-color",
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
attributeChangedCallback(name: DataAttributes, oldValue: string, newValue: string) {
|
||||||
if (name === "data-button-text") {
|
|
||||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("#button");
|
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("#button");
|
||||||
|
const buttonWrapperEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
||||||
|
const buttonIconEl = this.shadowRoot?.querySelector<HTMLElement>("#button-icon");
|
||||||
|
|
||||||
if (!buttonEl) {
|
if (!buttonEl) {
|
||||||
throw new Error("Button not found");
|
throw new Error("#button not found");
|
||||||
}
|
}
|
||||||
|
if (!buttonWrapperEl) {
|
||||||
|
throw new Error("button element not found");
|
||||||
|
}
|
||||||
|
if (!buttonIconEl) {
|
||||||
|
throw new Error("#button-icon not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name === "data-button-text") {
|
||||||
buttonEl.textContent = newValue;
|
buttonEl.textContent = newValue;
|
||||||
} else if (name === "data-hide-button-icon") {
|
} else if (name === "data-hide-button-icon") {
|
||||||
const buttonIconEl = this.shadowRoot?.querySelector<HTMLElement>("#button-icon");
|
|
||||||
if (!buttonIconEl) {
|
|
||||||
throw new Error("Button not found");
|
|
||||||
}
|
|
||||||
buttonIconEl.style.display = newValue == "true" ? "none" : "block";
|
buttonIconEl.style.display = newValue == "true" ? "none" : "block";
|
||||||
} else if (name === "data-button-position") {
|
} else if (name === "data-button-position") {
|
||||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
buttonWrapperEl.className = FloatingButton.updatedClassString(newValue, buttonWrapperEl.className);
|
||||||
if (!buttonEl) {
|
|
||||||
throw new Error("Button not found");
|
|
||||||
}
|
|
||||||
buttonEl.className = FloatingButton.updatedClassString(newValue, buttonEl.className);
|
|
||||||
} else if (name === "data-button-color") {
|
} else if (name === "data-button-color") {
|
||||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
buttonWrapperEl.style.backgroundColor = newValue;
|
||||||
if (!buttonEl) {
|
|
||||||
throw new Error("Button not found");
|
|
||||||
}
|
|
||||||
buttonEl.style.backgroundColor = newValue;
|
|
||||||
} else if (name === "data-button-text-color") {
|
} else if (name === "data-button-text-color") {
|
||||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
buttonWrapperEl.style.color = newValue;
|
||||||
if (!buttonEl) {
|
} else {
|
||||||
throw new Error("Button not found");
|
console.log("Unknown attribute changed", name, oldValue, newValue);
|
||||||
}
|
|
||||||
buttonEl.style.color = newValue;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertHasShadowRoot(): asserts this is HTMLElement & { shadowRoot: ShadowRoot } {
|
||||||
|
if (!this.shadowRoot) {
|
||||||
|
throw new Error("No shadow root");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const buttonText = this.dataset["buttonText"];
|
const dataset = this.dataset as FloatingButtonDataset;
|
||||||
const buttonPosition = this.dataset["buttonPosition"];
|
const buttonText = dataset["buttonText"];
|
||||||
const buttonColor = this.dataset["buttonColor"];
|
const buttonPosition = dataset["buttonPosition"];
|
||||||
const buttonTextColor = this.dataset["buttonTextColor"];
|
const buttonColor = dataset["buttonColor"];
|
||||||
|
const buttonTextColor = dataset["buttonTextColor"];
|
||||||
|
|
||||||
//TODO: Logic is duplicated over HTML generation and attribute change, keep it at one place
|
//TODO: Logic is duplicated over HTML generation and attribute change, keep it at one place
|
||||||
const buttonHtml = `<style>${window.Cal!.__css}</style> ${getFloatingButtonHtml({
|
const buttonHtml = `<style>${window.Cal.__css}</style> ${getFloatingButtonHtml({
|
||||||
buttonText: buttonText!,
|
buttonText: buttonText,
|
||||||
buttonClasses: [FloatingButton.updatedClassString(buttonPosition!, "")],
|
buttonClasses: [FloatingButton.updatedClassString(buttonPosition, "")],
|
||||||
buttonColor: buttonColor!,
|
buttonColor: buttonColor,
|
||||||
buttonTextColor: buttonTextColor!,
|
buttonTextColor: buttonTextColor,
|
||||||
})}`;
|
})}`;
|
||||||
|
|
||||||
this.attachShadow({ mode: "open" });
|
this.attachShadow({ mode: "open" });
|
||||||
this.shadowRoot!.innerHTML = buttonHtml;
|
this.assertHasShadowRoot();
|
||||||
|
this.shadowRoot.innerHTML = buttonHtml;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,31 +1,42 @@
|
|||||||
import loaderCss from "../loader.css";
|
import loaderCss from "../loader.css?inline";
|
||||||
import { getErrorString } from "../utils";
|
import { getErrorString } from "../utils";
|
||||||
import inlineHtml from "./inlineHtml";
|
import inlineHtml from "./inlineHtml";
|
||||||
|
|
||||||
export class Inline extends HTMLElement {
|
export class Inline extends HTMLElement {
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
//@ts-ignore
|
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
return ["loading"];
|
return ["loading"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertHasShadowRoot(): asserts this is HTMLElement & { shadowRoot: ShadowRoot } {
|
||||||
|
if (!this.shadowRoot) {
|
||||||
|
throw new Error("No shadow root");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||||
|
this.assertHasShadowRoot();
|
||||||
|
const loaderEl = this.shadowRoot.querySelector<HTMLElement>(".loader");
|
||||||
|
const errorEl = this.shadowRoot.querySelector<HTMLElement>("#error");
|
||||||
|
const slotEl = this.shadowRoot.querySelector<HTMLElement>("slot");
|
||||||
|
if (!loaderEl || !slotEl || !errorEl) {
|
||||||
|
throw new Error("One of loaderEl, slotEl or errorEl is missing");
|
||||||
|
}
|
||||||
if (name === "loading") {
|
if (name === "loading") {
|
||||||
if (newValue == "done") {
|
if (newValue == "done") {
|
||||||
this.shadowRoot!.querySelector<HTMLElement>(".loader")!.style.display = "none";
|
loaderEl.style.display = "none";
|
||||||
} else if (newValue === "failed") {
|
} else if (newValue === "failed") {
|
||||||
this.shadowRoot!.querySelector<HTMLElement>(".loader")!.style.display = "none";
|
loaderEl.style.display = "none";
|
||||||
this.shadowRoot!.querySelector<HTMLElement>("#error")!.style.display = "block";
|
slotEl.style.visibility = "hidden";
|
||||||
this.shadowRoot!.querySelector<HTMLElement>("slot")!.style.visibility = "hidden";
|
errorEl.style.display = "block";
|
||||||
const errorString = getErrorString(this.dataset.errorCode);
|
const errorString = getErrorString(this.dataset.errorCode);
|
||||||
this.shadowRoot!.querySelector<HTMLElement>("#error")!.innerText = errorString;
|
errorEl.innerText = errorString;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
this.attachShadow({ mode: "open" });
|
this.attachShadow({ mode: "open" });
|
||||||
this.shadowRoot!.innerHTML = `<style>${
|
this.assertHasShadowRoot();
|
||||||
window.Cal!.__css
|
this.shadowRoot.innerHTML = `<style>${window.Cal.__css}</style><style>${loaderCss}</style>${inlineHtml}`;
|
||||||
}</style><style>${loaderCss}</style>${inlineHtml}`;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -101,7 +101,7 @@ export class ModalBox extends HTMLElement {
|
|||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
const modalHtml = `<style>${window.Cal!.__css}</style><style>${loaderCss}</style>${modalBoxHtml}`;
|
const modalHtml = `<style>${window.Cal.__css}</style><style>${loaderCss}</style>${modalBoxHtml}`;
|
||||||
this.attachShadow({ mode: "open" });
|
this.attachShadow({ mode: "open" });
|
||||||
ModalBox.htmlOverflow = document.body.style.overflow;
|
ModalBox.htmlOverflow = document.body.style.overflow;
|
||||||
document.body.style.overflow = "hidden";
|
document.body.style.overflow = "hidden";
|
||||||
|
|||||||
@@ -2,18 +2,40 @@ import { useRouter } from "next/router";
|
|||||||
import type { CSSProperties } from "react";
|
import type { CSSProperties } from "react";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
import type { Message } from "./embed";
|
||||||
import { sdkActionManager } from "./sdk-event";
|
import { sdkActionManager } from "./sdk-event";
|
||||||
|
|
||||||
export interface UiConfig {
|
export type UiConfig = {
|
||||||
hideEventTypeDetails?: boolean;
|
hideEventTypeDetails?: boolean;
|
||||||
theme?: "dark" | "light" | "auto";
|
theme?: "dark" | "light" | "auto";
|
||||||
styles?: EmbedStyles;
|
styles?: EmbedStyles & EmbedNonStylesConfig;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
type SetStyles = React.Dispatch<React.SetStateAction<EmbedStyles>>;
|
||||||
|
type setNonStylesConfig = React.Dispatch<React.SetStateAction<EmbedNonStylesConfig>>;
|
||||||
|
|
||||||
|
const embedStore = {
|
||||||
|
// Store all embed styles here so that as and when new elements are mounted, styles can be applied to it.
|
||||||
|
styles: {} as EmbedStyles | undefined,
|
||||||
|
nonStyles: {} as EmbedNonStylesConfig | undefined,
|
||||||
|
namespace: null as string | null,
|
||||||
|
embedType: undefined as undefined | null | string,
|
||||||
|
// Store all React State setters here.
|
||||||
|
reactStylesStateSetters: {} as Record<keyof EmbedStyles, SetStyles>,
|
||||||
|
reactNonStylesStateSetters: {} as Record<keyof EmbedNonStylesConfig, setNonStylesConfig>,
|
||||||
|
parentInformedAboutContentHeight: false,
|
||||||
|
windowLoadEventFired: false,
|
||||||
|
setTheme: undefined as ((arg0: string) => void) | undefined,
|
||||||
|
theme: undefined as UiConfig["theme"],
|
||||||
|
uiConfig: undefined as Omit<UiConfig, "styles" | "theme"> | undefined,
|
||||||
|
setUiConfig: undefined as ((arg0: UiConfig) => void) | undefined,
|
||||||
|
};
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
CalEmbed: {
|
CalEmbed: {
|
||||||
__logQueue?: any[];
|
__logQueue?: unknown[];
|
||||||
embedStore: any;
|
embedStore: typeof embedStore;
|
||||||
};
|
};
|
||||||
CalComPageStatus: string;
|
CalComPageStatus: string;
|
||||||
isEmbed: () => boolean;
|
isEmbed: () => boolean;
|
||||||
@@ -23,28 +45,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const embedStore = {
|
|
||||||
// Store all embed styles here so that as and when new elements are mounted, styles can be applied to it.
|
|
||||||
styles: {},
|
|
||||||
namespace: null,
|
|
||||||
embedType: undefined,
|
|
||||||
// Store all React State setters here.
|
|
||||||
reactStylesStateSetters: {},
|
|
||||||
parentInformedAboutContentHeight: false,
|
|
||||||
windowLoadEventFired: false,
|
|
||||||
} as {
|
|
||||||
styles: UiConfig["styles"];
|
|
||||||
namespace: string | null;
|
|
||||||
embedType: undefined | null | string;
|
|
||||||
reactStylesStateSetters: any;
|
|
||||||
parentInformedAboutContentHeight: boolean;
|
|
||||||
windowLoadEventFired: boolean;
|
|
||||||
theme?: UiConfig["theme"];
|
|
||||||
uiConfig?: Omit<UiConfig, "styles" | "theme">;
|
|
||||||
setTheme?: (arg0: string) => void;
|
|
||||||
setUiConfig?: (arg0: UiConfig) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
let isSafariBrowser = false;
|
let isSafariBrowser = false;
|
||||||
const isBrowser = typeof window !== "undefined";
|
const isBrowser = typeof window !== "undefined";
|
||||||
|
|
||||||
@@ -58,7 +58,7 @@ if (isBrowser) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function runAsap(fn: (...arg: any) => void) {
|
function runAsap(fn: (...arg: unknown[]) => void) {
|
||||||
if (isSafariBrowser) {
|
if (isSafariBrowser) {
|
||||||
// https://adpiler.com/blog/the-full-solution-why-do-animations-run-slower-in-safari/
|
// https://adpiler.com/blog/the-full-solution-why-do-animations-run-slower-in-safari/
|
||||||
return setTimeout(fn, 50);
|
return setTimeout(fn, 50);
|
||||||
@@ -66,7 +66,7 @@ function runAsap(fn: (...arg: any) => void) {
|
|||||||
return requestAnimationFrame(fn);
|
return requestAnimationFrame(fn);
|
||||||
}
|
}
|
||||||
|
|
||||||
function log(...args: any[]) {
|
function log(...args: unknown[]) {
|
||||||
if (isBrowser) {
|
if (isBrowser) {
|
||||||
const namespace = getNamespace();
|
const namespace = getNamespace();
|
||||||
|
|
||||||
@@ -95,7 +95,7 @@ interface EmbedStyles {
|
|||||||
}
|
}
|
||||||
interface EmbedNonStylesConfig {
|
interface EmbedNonStylesConfig {
|
||||||
/** Default would be center */
|
/** Default would be center */
|
||||||
align: "left";
|
align?: "left";
|
||||||
branding?: {
|
branding?: {
|
||||||
brandColor?: string;
|
brandColor?: string;
|
||||||
lightColor?: string;
|
lightColor?: string;
|
||||||
@@ -108,10 +108,10 @@ interface EmbedNonStylesConfig {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => {
|
const setEmbedStyles = (stylesConfig: EmbedStyles) => {
|
||||||
embedStore.styles = stylesConfig;
|
embedStore.styles = stylesConfig;
|
||||||
for (const [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) {
|
for (const [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) {
|
||||||
(setEmbedStyle as any)((styles: any) => {
|
setEmbedStyle((styles) => {
|
||||||
return {
|
return {
|
||||||
...styles,
|
...styles,
|
||||||
...stylesConfig,
|
...stylesConfig,
|
||||||
@@ -120,15 +120,48 @@ const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const registerNewSetter = (elementName: keyof EmbedStyles | keyof EmbedNonStylesConfig, setStyles: any) => {
|
const setEmbedNonStyles = (stylesConfig: EmbedNonStylesConfig) => {
|
||||||
embedStore.reactStylesStateSetters[elementName] = setStyles;
|
embedStore.nonStyles = stylesConfig;
|
||||||
// It's possible that 'ui' instruction has already been processed and the registration happened due to some action by the user in iframe.
|
for (const [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) {
|
||||||
// So, we should call the setter immediately with available embedStyles
|
setEmbedStyle((styles) => {
|
||||||
setStyles(embedStore.styles);
|
return {
|
||||||
|
...styles,
|
||||||
|
...stylesConfig,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeFromEmbedStylesSetterMap = (elementName: keyof EmbedStyles | keyof EmbedNonStylesConfig) => {
|
const registerNewSetter = (
|
||||||
delete embedStore.reactStylesStateSetters[elementName];
|
registration:
|
||||||
|
| {
|
||||||
|
elementName: keyof EmbedStyles;
|
||||||
|
setState: SetStyles;
|
||||||
|
styles: true;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
elementName: keyof EmbedNonStylesConfig;
|
||||||
|
setState: setNonStylesConfig;
|
||||||
|
styles: false;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
// It's possible that 'ui' instruction has already been processed and the registration happened due to some action by the user in iframe.
|
||||||
|
// So, we should call the setter immediately with available embedStyles
|
||||||
|
if (registration.styles) {
|
||||||
|
embedStore.reactStylesStateSetters[registration.elementName as keyof EmbedStyles] = registration.setState;
|
||||||
|
registration.setState(embedStore.styles || {});
|
||||||
|
return () => {
|
||||||
|
delete embedStore.reactStylesStateSetters[registration.elementName];
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
embedStore.reactNonStylesStateSetters[registration.elementName as keyof EmbedNonStylesConfig] =
|
||||||
|
registration.setState;
|
||||||
|
registration.setState(embedStore.nonStyles || {});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
delete embedStore.reactNonStylesStateSetters[registration.elementName];
|
||||||
|
};
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
function isValidNamespace(ns: string | null | undefined) {
|
function isValidNamespace(ns: string | null | undefined) {
|
||||||
@@ -155,33 +188,28 @@ export const useEmbedUiConfig = () => {
|
|||||||
|
|
||||||
// TODO: Make it usable as an attribute directly instead of styles value. It would allow us to go beyond styles e.g. for debugging we can add a special attribute indentifying the element on which UI config has been applied
|
// TODO: Make it usable as an attribute directly instead of styles value. It would allow us to go beyond styles e.g. for debugging we can add a special attribute indentifying the element on which UI config has been applied
|
||||||
export const useEmbedStyles = (elementName: keyof EmbedStyles) => {
|
export const useEmbedStyles = (elementName: keyof EmbedStyles) => {
|
||||||
const [styles, setStyles] = useState({} as EmbedStyles);
|
const [, setStyles] = useState<EmbedStyles>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerNewSetter(elementName, setStyles);
|
return registerNewSetter({ elementName, setState: setStyles, styles: true });
|
||||||
// It's important to have an element's embed style be required in only one component. If due to any reason it is required in multiple components, we would override state setter.
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
return () => {
|
|
||||||
// Once the component is unmounted, we can remove that state setter.
|
|
||||||
removeFromEmbedStylesSetterMap(elementName);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
const styles = embedStore.styles || {};
|
||||||
|
// Always read the data from global embedStore so that even across components, the same data is used.
|
||||||
return styles[elementName] || {};
|
return styles[elementName] || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useEmbedNonStylesConfig = (elementName: keyof EmbedNonStylesConfig) => {
|
export const useEmbedNonStylesConfig = (elementName: keyof EmbedNonStylesConfig) => {
|
||||||
const [styles, setStyles] = useState({} as EmbedNonStylesConfig);
|
const [, setNonStyles] = useState({} as EmbedNonStylesConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
registerNewSetter(elementName, setStyles);
|
return registerNewSetter({ elementName, setState: setNonStyles, styles: false });
|
||||||
// It's important to have an element's embed style be required in only one component. If due to any reason it is required in multiple components, we would override state setter.
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
return () => {
|
|
||||||
// Once the component is unmounted, we can remove that state setter.
|
|
||||||
removeFromEmbedStylesSetterMap(elementName);
|
|
||||||
};
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return styles[elementName] || {};
|
// Always read the data from global embedStore so that even across components, the same data is used.
|
||||||
|
const nonStyles = embedStore.nonStyles || {};
|
||||||
|
return nonStyles[elementName] || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useIsBackgroundTransparent = () => {
|
export const useIsBackgroundTransparent = () => {
|
||||||
@@ -251,8 +279,8 @@ function unhideBody() {
|
|||||||
document.body.style.visibility = "visible";
|
document.body.style.visibility = "visible";
|
||||||
}
|
}
|
||||||
|
|
||||||
// If you add a method here, give type safety to parent manually by adding it to embed.ts. Look for "parentKnowsIframeReady" in it
|
// It is a map of methods that can be called by parent using doInIframe({method: "methodName", arg: "argument"})
|
||||||
export const methods = {
|
const methods = {
|
||||||
ui: function style(uiConfig: UiConfig) {
|
ui: function style(uiConfig: UiConfig) {
|
||||||
// TODO: Create automatic logger for all methods. Useful for debugging.
|
// TODO: Create automatic logger for all methods. Useful for debugging.
|
||||||
log("Method: ui called", uiConfig);
|
log("Method: ui called", uiConfig);
|
||||||
@@ -277,8 +305,10 @@ export const methods = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setEmbedStyles(stylesConfig || {});
|
setEmbedStyles(stylesConfig || {});
|
||||||
|
setEmbedNonStyles(stylesConfig || {});
|
||||||
},
|
},
|
||||||
parentKnowsIframeReady: () => {
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
parentKnowsIframeReady: (_unused: unknown) => {
|
||||||
log("Method: `parentKnowsIframeReady` called");
|
log("Method: `parentKnowsIframeReady` called");
|
||||||
runAsap(function tryInformingLinkReady() {
|
runAsap(function tryInformingLinkReady() {
|
||||||
// TODO: Do it by attaching a listener for change in parentInformedAboutContentHeight
|
// TODO: Do it by attaching a listener for change in parentInformedAboutContentHeight
|
||||||
@@ -293,7 +323,14 @@ export const methods = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const messageParent = (data: any) => {
|
export type InterfaceWithParent = {
|
||||||
|
// Ensure that only one argument is read by the method
|
||||||
|
[key in keyof typeof methods]: (firstAndOnlyArg: Parameters<(typeof methods)[key]>[number]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const interfaceWithParent: InterfaceWithParent = methods;
|
||||||
|
|
||||||
|
const messageParent = (data: CustomEvent["detail"]) => {
|
||||||
parent.postMessage(
|
parent.postMessage(
|
||||||
{
|
{
|
||||||
originator: "CAL",
|
originator: "CAL",
|
||||||
@@ -345,11 +382,13 @@ function keepParentInformedAboutDimensionChanges() {
|
|||||||
const contentHeight = Math.ceil(
|
const contentHeight = Math.ceil(
|
||||||
parseFloat(mainElementStyles.height) +
|
parseFloat(mainElementStyles.height) +
|
||||||
parseFloat(mainElementStyles.marginTop) +
|
parseFloat(mainElementStyles.marginTop) +
|
||||||
parseFloat(mainElementStyles.marginBottom));
|
parseFloat(mainElementStyles.marginBottom)
|
||||||
|
);
|
||||||
const contentWidth = Math.ceil(
|
const contentWidth = Math.ceil(
|
||||||
parseFloat(mainElementStyles.width) +
|
parseFloat(mainElementStyles.width) +
|
||||||
parseFloat(mainElementStyles.marginLeft) +
|
parseFloat(mainElementStyles.marginLeft) +
|
||||||
parseFloat(mainElementStyles.marginRight));
|
parseFloat(mainElementStyles.marginRight)
|
||||||
|
);
|
||||||
|
|
||||||
// During first render let iframe tell parent that how much is the expected height to avoid scroll.
|
// During first render let iframe tell parent that how much is the expected height to avoid scroll.
|
||||||
// Parent would set the same value as the height of iframe which would prevent scroll.
|
// Parent would set the same value as the height of iframe which would prevent scroll.
|
||||||
@@ -394,35 +433,18 @@ if (isBrowser) {
|
|||||||
|
|
||||||
sdkActionManager?.on("*", (e) => {
|
sdkActionManager?.on("*", (e) => {
|
||||||
const detail = e.detail;
|
const detail = e.detail;
|
||||||
//console.log(detail.fullType, detail.type, detail.data);
|
|
||||||
log(detail);
|
log(detail);
|
||||||
messageParent(detail);
|
messageParent(detail);
|
||||||
});
|
});
|
||||||
|
|
||||||
// This event should be fired whenever you want to let the content take automatic width which is available.
|
|
||||||
// Because on cal-iframe we set explicty width to make it look inline and part of page, there is never space available for content to automatically expand
|
|
||||||
// This is a HACK to quickly tell iframe to go full width and let iframe content adapt to that and set new width.
|
|
||||||
sdkActionManager?.on("__refreshWidth", () => {
|
|
||||||
// sdkActionManager?.fire("__dimensionChanged", {
|
|
||||||
// iframeWidth: 100,
|
|
||||||
// __unit: "%",
|
|
||||||
// });
|
|
||||||
// runAsap(() => {
|
|
||||||
// sdkActionManager?.fire("__dimensionChanged", {
|
|
||||||
// iframeWidth: 100,
|
|
||||||
// __unit: "%",
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("message", (e) => {
|
window.addEventListener("message", (e) => {
|
||||||
const data: Record<string, any> = e.data;
|
const data: Message = e.data;
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const method: keyof typeof methods = data.method;
|
const method: keyof typeof interfaceWithParent = data.method;
|
||||||
if (data.originator === "CAL" && typeof method === "string") {
|
if (data.originator === "CAL" && typeof method === "string") {
|
||||||
methods[method]?.(data.arg);
|
interfaceWithParent[method]?.(data.arg as never);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,20 @@
|
|||||||
|
/// <reference types="../env" />
|
||||||
import { FloatingButton } from "./FloatingButton/FloatingButton";
|
import { FloatingButton } from "./FloatingButton/FloatingButton";
|
||||||
import { Inline } from "./Inline/inline";
|
import { Inline } from "./Inline/inline";
|
||||||
import { ModalBox } from "./ModalBox/ModalBox";
|
import { ModalBox } from "./ModalBox/ModalBox";
|
||||||
import type { methods, UiConfig } from "./embed-iframe";
|
import type { InterfaceWithParent, interfaceWithParent, UiConfig } from "./embed-iframe";
|
||||||
import css from "./embed.css";
|
import css from "./embed.css";
|
||||||
|
import type { EventData, EventDataMap } from "./sdk-action-manager";
|
||||||
import { SdkActionManager } from "./sdk-action-manager";
|
import { SdkActionManager } from "./sdk-action-manager";
|
||||||
import allCss from "./tailwind.generated.css";
|
import allCss from "./tailwind.generated.css?inline";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type Rest<T extends any[]> = T extends [any, ...infer U] ? U : never;
|
||||||
|
export type Message = {
|
||||||
|
originator: string;
|
||||||
|
method: keyof InterfaceWithParent;
|
||||||
|
arg: InterfaceWithParent[keyof InterfaceWithParent];
|
||||||
|
};
|
||||||
// HACK: Redefine and don't import WEBAPP_URL as it causes import statement to be present in built file.
|
// HACK: Redefine and don't import WEBAPP_URL as it causes import statement to be present in built file.
|
||||||
// This is happening because we are not able to generate an App and a lib using single Vite Config.
|
// This is happening because we are not able to generate an App and a lib using single Vite Config.
|
||||||
const WEBAPP_URL =
|
const WEBAPP_URL =
|
||||||
@@ -18,10 +27,20 @@ customElements.define("cal-inline", Inline);
|
|||||||
declare module "*.css";
|
declare module "*.css";
|
||||||
type Namespace = string;
|
type Namespace = string;
|
||||||
type Config = {
|
type Config = {
|
||||||
origin: string;
|
calOrigin: string;
|
||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
uiDebug?: boolean;
|
uiDebug?: boolean;
|
||||||
};
|
};
|
||||||
|
type InitArgConfig = Partial<Config> & {
|
||||||
|
origin?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type DoInIframeArg = {
|
||||||
|
[K in keyof typeof interfaceWithParent]: {
|
||||||
|
method: K;
|
||||||
|
arg?: Parameters<(typeof interfaceWithParent)[K]>[0];
|
||||||
|
};
|
||||||
|
}[keyof typeof interfaceWithParent];
|
||||||
|
|
||||||
const globalCal = window.Cal;
|
const globalCal = window.Cal;
|
||||||
if (!globalCal || !globalCal.q) {
|
if (!globalCal || !globalCal.q) {
|
||||||
@@ -35,9 +54,22 @@ globalCal.fingerprint = import.meta.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as strin
|
|||||||
globalCal.__css = allCss;
|
globalCal.__css = allCss;
|
||||||
document.head.appendChild(document.createElement("style")).innerHTML = css;
|
document.head.appendChild(document.createElement("style")).innerHTML = css;
|
||||||
|
|
||||||
function log(...args: any[]) {
|
function log(...args: unknown[]) {
|
||||||
console.log(...args);
|
console.log(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
|
type ValidationSchemaPropType = string | Function;
|
||||||
|
|
||||||
|
type ValidationSchema = {
|
||||||
|
required?: boolean;
|
||||||
|
props?: Record<
|
||||||
|
string,
|
||||||
|
ValidationSchema & {
|
||||||
|
type: ValidationSchemaPropType | ValidationSchemaPropType[];
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* //TODO: Warn about extra properties not part of schema. Helps in fixing wrong expectations
|
* //TODO: Warn about extra properties not part of schema. Helps in fixing wrong expectations
|
||||||
* A very simple data validator written with intention of keeping payload size low.
|
* A very simple data validator written with intention of keeping payload size low.
|
||||||
@@ -45,8 +77,8 @@ function log(...args: any[]) {
|
|||||||
* @param data
|
* @param data
|
||||||
* @param schema
|
* @param schema
|
||||||
*/
|
*/
|
||||||
function validate(data: any, schema: Record<"props" | "required", any>) {
|
function validate(data: Record<string, unknown>, schema: ValidationSchema) {
|
||||||
function checkType(value: any, expectedType: any) {
|
function checkType(value: unknown, expectedType: ValidationSchemaPropType) {
|
||||||
if (typeof expectedType === "string") {
|
if (typeof expectedType === "string") {
|
||||||
return typeof value == expectedType;
|
return typeof value == expectedType;
|
||||||
} else {
|
} else {
|
||||||
@@ -54,7 +86,7 @@ function validate(data: any, schema: Record<"props" | "required", any>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isUndefined(data: any) {
|
function isUndefined(data: unknown) {
|
||||||
return typeof data === "undefined";
|
return typeof data === "undefined";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +94,7 @@ function validate(data: any, schema: Record<"props" | "required", any>) {
|
|||||||
throw new Error("Argument is required");
|
throw new Error("Argument is required");
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [prop, propSchema] of Object.entries<Record<"type" | "required", any>>(schema.props)) {
|
for (const [prop, propSchema] of Object.entries(schema.props || {})) {
|
||||||
if (propSchema.required && isUndefined(data[prop])) {
|
if (propSchema.required && isUndefined(data[prop])) {
|
||||||
throw new Error(`"${prop}" is required`);
|
throw new Error(`"${prop}" is required`);
|
||||||
}
|
}
|
||||||
@@ -82,9 +114,23 @@ function validate(data: any, schema: Record<"props" | "required", any>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Instruction = [method: string, argument: any] | [method: string, argument: any][];
|
type SingleInstructionMap = {
|
||||||
|
// TODO: This makes api("on", {}) loose it's generic type. Find a way to fix it.
|
||||||
|
// e.g. api("on", { action: "__dimensionChanged", callback: (e) => { /* `e.detail.data` has all possible values for all events/actions */} });
|
||||||
|
[K in keyof CalApi]: CalApi[K] extends (...args: never[]) => void ? [K, ...Parameters<CalApi[K]>] : never;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SingleInstruction = SingleInstructionMap[keyof SingleInstructionMap];
|
||||||
|
|
||||||
|
export type Instruction = SingleInstruction | SingleInstruction[];
|
||||||
export type InstructionQueue = Instruction[];
|
export type InstructionQueue = Instruction[];
|
||||||
|
|
||||||
|
type PrefillAndIframeAttrsConfig = Record<string, string | string[] | Record<string, string>> & {
|
||||||
|
iframeAttrs?: Record<string, string> & {
|
||||||
|
id?: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export class Cal {
|
export class Cal {
|
||||||
iframe?: HTMLIFrameElement;
|
iframe?: HTMLIFrameElement;
|
||||||
|
|
||||||
@@ -100,35 +146,42 @@ export class Cal {
|
|||||||
|
|
||||||
iframeReady!: boolean;
|
iframeReady!: boolean;
|
||||||
|
|
||||||
iframeDoQueue: { method: keyof typeof methods; arg: any }[] = [];
|
iframeDoQueue: DoInIframeArg[] = [];
|
||||||
|
|
||||||
|
api: CalApi;
|
||||||
|
|
||||||
static actionsManagers: Record<Namespace, SdkActionManager>;
|
static actionsManagers: Record<Namespace, SdkActionManager>;
|
||||||
|
|
||||||
static getQueryObject(config: Record<string, string>) {
|
static getQueryObject(config: PrefillAndIframeAttrsConfig) {
|
||||||
config = config || {};
|
config = config || {};
|
||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
// guests is better for API but Booking Page accepts guest. So do the mapping
|
// guests is better for API but Booking Page accepts guest. So do the mapping
|
||||||
guest: config.guests ?? undefined,
|
guest: config.guests ?? undefined,
|
||||||
};
|
} as PrefillAndIframeAttrsConfig & { guest?: string | string[] };
|
||||||
}
|
}
|
||||||
|
|
||||||
processInstruction(instruction: Instruction) {
|
processInstruction(instructionAsArgs: IArguments | Instruction) {
|
||||||
instruction = [].slice.call(instruction, 0);
|
// The instruction is actually an array-like object(arguments). Make it an array.
|
||||||
if (instruction[0] instanceof Array) {
|
const instruction: Instruction = [].slice.call(instructionAsArgs);
|
||||||
|
// If there are multiple instructions in the array, process them one by one
|
||||||
|
if (typeof instruction[0] !== "string") {
|
||||||
// It is an instruction
|
// It is an instruction
|
||||||
instruction.forEach((instruction) => {
|
instruction.forEach((instruction) => {
|
||||||
this.processInstruction(instruction);
|
this.processInstruction(instruction);
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [method, ...args] = instruction;
|
const [method, ...args] = instruction;
|
||||||
if (!this[method]) {
|
if (!this.api[method]) {
|
||||||
// Instead of throwing error, log and move forward in the queue
|
// Instead of throwing error, log and move forward in the queue
|
||||||
log(`Instruction ${method} not FOUND`);
|
log(`Instruction ${method} not FOUND`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
(this[method] as (...args: any[]) => void)(...args);
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore There can be any method which can have any number of arguments.
|
||||||
|
this.api[method](...args);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Instead of throwing error, log and move forward in the queue
|
// Instead of throwing error, log and move forward in the queue
|
||||||
log(`Instruction couldn't be executed`, e);
|
log(`Instruction couldn't be executed`, e);
|
||||||
@@ -136,7 +189,7 @@ export class Cal {
|
|||||||
return instruction;
|
return instruction;
|
||||||
}
|
}
|
||||||
|
|
||||||
processQueue(queue: InstructionQueue) {
|
processQueue(queue: IArguments[]) {
|
||||||
queue.forEach((instruction) => {
|
queue.forEach((instruction) => {
|
||||||
this.processInstruction(instruction);
|
this.processInstruction(instruction);
|
||||||
});
|
});
|
||||||
@@ -153,9 +206,11 @@ export class Cal {
|
|||||||
createIframe({
|
createIframe({
|
||||||
calLink,
|
calLink,
|
||||||
queryObject = {},
|
queryObject = {},
|
||||||
|
calOrigin,
|
||||||
}: {
|
}: {
|
||||||
calLink: string;
|
calLink: string;
|
||||||
queryObject?: Record<string, string | string[] | Record<string, string>>;
|
queryObject?: PrefillAndIframeAttrsConfig & { guest?: string | string[] };
|
||||||
|
calOrigin?: string;
|
||||||
}) {
|
}) {
|
||||||
const iframe = (this.iframe = document.createElement("iframe"));
|
const iframe = (this.iframe = document.createElement("iframe"));
|
||||||
iframe.className = "cal-embed";
|
iframe.className = "cal-embed";
|
||||||
@@ -163,7 +218,7 @@ export class Cal {
|
|||||||
const config = this.getConfig();
|
const config = this.getConfig();
|
||||||
const { iframeAttrs, ...restQueryObject } = queryObject;
|
const { iframeAttrs, ...restQueryObject } = queryObject;
|
||||||
|
|
||||||
if (iframeAttrs && typeof iframeAttrs !== "string" && !(iframeAttrs instanceof Array)) {
|
if (iframeAttrs && iframeAttrs.id) {
|
||||||
iframe.setAttribute("id", iframeAttrs.id);
|
iframe.setAttribute("id", iframeAttrs.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +235,7 @@ export class Cal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const urlInstance = new URL(`${config.origin}/${calLink}`);
|
const urlInstance = new URL(`${calOrigin || config.calOrigin}/${calLink}`);
|
||||||
if (!urlInstance.pathname.endsWith("embed")) {
|
if (!urlInstance.pathname.endsWith("embed")) {
|
||||||
// TODO: Make a list of patterns that are embeddable. All except that should be allowed with a warning that "The page isn't optimized for embedding"
|
// TODO: Make a list of patterns that are embeddable. All except that should be allowed with a warning that "The page isn't optimized for embedding"
|
||||||
urlInstance.pathname = `${urlInstance.pathname}/embed`;
|
urlInstance.pathname = `${urlInstance.pathname}/embed`;
|
||||||
@@ -203,252 +258,34 @@ export class Cal {
|
|||||||
return iframe;
|
return iframe;
|
||||||
}
|
}
|
||||||
|
|
||||||
init(namespaceOrConfig?: string | Config, config: Config = {} as Config) {
|
|
||||||
if (typeof namespaceOrConfig !== "string") {
|
|
||||||
config = (namespaceOrConfig || {}) as Config;
|
|
||||||
}
|
|
||||||
const { origin, ...restConfig } = config;
|
|
||||||
if (origin) {
|
|
||||||
this.__config.origin = origin;
|
|
||||||
}
|
|
||||||
this.__config = { ...this.__config, ...restConfig };
|
|
||||||
}
|
|
||||||
|
|
||||||
getConfig() {
|
getConfig() {
|
||||||
return this.__config;
|
return this.__config;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Maintain exposed methods in a separate namespace, so that unexpected methods don't become instructions
|
doInIframe(doInIframeArg: DoInIframeArg) {
|
||||||
|
|
||||||
/**
|
|
||||||
* It is an instruction that adds embed iframe inline as last child of the element
|
|
||||||
*/
|
|
||||||
inline({
|
|
||||||
calLink,
|
|
||||||
elementOrSelector,
|
|
||||||
config,
|
|
||||||
}: {
|
|
||||||
calLink: string;
|
|
||||||
elementOrSelector: string | HTMLElement;
|
|
||||||
config: Record<string, string>;
|
|
||||||
}) {
|
|
||||||
// eslint-disable-next-line prefer-rest-params
|
|
||||||
validate(arguments[0], {
|
|
||||||
required: true,
|
|
||||||
props: {
|
|
||||||
calLink: {
|
|
||||||
// TODO: Add a special type calLink for it and validate that it doesn't start with / or https?://
|
|
||||||
required: true,
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
elementOrSelector: {
|
|
||||||
required: true,
|
|
||||||
type: ["string", HTMLElement],
|
|
||||||
},
|
|
||||||
config: {
|
|
||||||
required: false,
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
config = config || {};
|
|
||||||
// Keeping auto-scroll disabled for two reasons:
|
|
||||||
// - If user scrolls the content to an appropriate position, it again resets it to default position which might not be for the liking of the user
|
|
||||||
// - Sometimes, the position can be wrong(e.g. if there is a fixed position header on top coming above the iframe content).
|
|
||||||
// Best solution might be to autoscroll only if the iframe is not fully visible, detection of full visibility might be tough
|
|
||||||
|
|
||||||
// We need to keep in mind that autoscroll is meant to solve the problem when on a certain view(which is availability page right now), the height goes too high and then suddenly it becomes normal
|
|
||||||
(config as unknown as any).__autoScroll = !!(config as unknown as any).__autoScroll;
|
|
||||||
config.embedType = "inline";
|
|
||||||
const iframe = this.createIframe({ calLink, queryObject: Cal.getQueryObject(config) });
|
|
||||||
iframe.style.height = "100%";
|
|
||||||
iframe.style.width = "100%";
|
|
||||||
const element =
|
|
||||||
elementOrSelector instanceof HTMLElement
|
|
||||||
? elementOrSelector
|
|
||||||
: document.querySelector(elementOrSelector);
|
|
||||||
if (!element) {
|
|
||||||
throw new Error("Element not found");
|
|
||||||
}
|
|
||||||
element.classList.add("cal-inline-container");
|
|
||||||
const template = document.createElement("template");
|
|
||||||
template.innerHTML = `<cal-inline style="max-height:inherit;height:inherit;min-height:inherit;display:flex;position:relative;flex-wrap:wrap;width:100%"></cal-inline><style>.cal-inline-container::-webkit-scrollbar{display:none}.cal-inline-container{scrollbar-width:none}</style>`;
|
|
||||||
this.inlineEl = template.content.children[0];
|
|
||||||
(this.inlineEl as unknown as any).__CalAutoScroll = config.__autoScroll;
|
|
||||||
this.inlineEl.appendChild(iframe);
|
|
||||||
element.appendChild(template.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
floatingButton({
|
|
||||||
calLink,
|
|
||||||
buttonText = "Book my Cal",
|
|
||||||
hideButtonIcon = false,
|
|
||||||
attributes,
|
|
||||||
buttonPosition = "bottom-right",
|
|
||||||
buttonColor = "rgb(0, 0, 0)",
|
|
||||||
buttonTextColor = "rgb(255, 255, 255)",
|
|
||||||
}: {
|
|
||||||
calLink: string;
|
|
||||||
buttonText?: string;
|
|
||||||
attributes?: Record<string, string>;
|
|
||||||
hideButtonIcon?: boolean;
|
|
||||||
buttonPosition?: "bottom-left" | "bottom-right";
|
|
||||||
buttonColor: string;
|
|
||||||
buttonTextColor: string;
|
|
||||||
}) {
|
|
||||||
// validate(arguments[0], {
|
|
||||||
// required: true,
|
|
||||||
// props: {
|
|
||||||
// calLink: {
|
|
||||||
// required: true,
|
|
||||||
// type: "string",
|
|
||||||
// },
|
|
||||||
// },
|
|
||||||
// });
|
|
||||||
let existingEl: HTMLElement | null = null;
|
|
||||||
|
|
||||||
if (attributes?.id) {
|
|
||||||
existingEl = document.getElementById(attributes.id);
|
|
||||||
}
|
|
||||||
let el: HTMLElement;
|
|
||||||
if (!existingEl) {
|
|
||||||
el = document.createElement("cal-floating-button");
|
|
||||||
el.dataset.calLink = calLink;
|
|
||||||
el.dataset.calNamespace = this.namespace;
|
|
||||||
if (attributes?.id) {
|
|
||||||
el.id = attributes.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
document.body.appendChild(el);
|
|
||||||
} else {
|
|
||||||
el = existingEl;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (buttonText) {
|
|
||||||
el.setAttribute("data-button-text", buttonText);
|
|
||||||
}
|
|
||||||
el.setAttribute("data-hide-button-icon", "" + hideButtonIcon);
|
|
||||||
el.setAttribute("data-button-position", "" + buttonPosition);
|
|
||||||
el.setAttribute("data-button-color", "" + buttonColor);
|
|
||||||
el.setAttribute("data-button-text-color", "" + buttonTextColor);
|
|
||||||
}
|
|
||||||
|
|
||||||
modal({ calLink, config = {}, uid }: { calLink: string; config?: Record<string, string>; uid: number }) {
|
|
||||||
const existingModalEl = document.querySelector(`cal-modal-box[uid="${uid}"]`);
|
|
||||||
if (existingModalEl) {
|
|
||||||
existingModalEl.setAttribute("state", "started");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
config.embedType = "modal";
|
|
||||||
const iframe = this.createIframe({ calLink, queryObject: Cal.getQueryObject(config) });
|
|
||||||
iframe.style.borderRadius = "8px";
|
|
||||||
|
|
||||||
iframe.style.height = "100%";
|
|
||||||
iframe.style.width = "100%";
|
|
||||||
const template = document.createElement("template");
|
|
||||||
template.innerHTML = `<cal-modal-box uid="${uid}"></cal-modal-box>`;
|
|
||||||
|
|
||||||
this.modalBox = template.content.children[0];
|
|
||||||
this.modalBox.appendChild(iframe);
|
|
||||||
this.actionManager.on("__closeIframe", () => {
|
|
||||||
this.modalBox.setAttribute("state", "closed");
|
|
||||||
});
|
|
||||||
document.body.appendChild(template.content);
|
|
||||||
}
|
|
||||||
|
|
||||||
on({
|
|
||||||
action,
|
|
||||||
callback,
|
|
||||||
}: {
|
|
||||||
action: Parameters<SdkActionManager["on"]>[0];
|
|
||||||
callback: Parameters<SdkActionManager["on"]>[1];
|
|
||||||
}) {
|
|
||||||
// eslint-disable-next-line prefer-rest-params
|
|
||||||
validate(arguments[0], {
|
|
||||||
required: true,
|
|
||||||
props: {
|
|
||||||
action: {
|
|
||||||
required: true,
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
callback: {
|
|
||||||
required: true,
|
|
||||||
type: Function,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.actionManager.on(action, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
off({
|
|
||||||
action,
|
|
||||||
callback,
|
|
||||||
}: {
|
|
||||||
action: Parameters<SdkActionManager["on"]>[0];
|
|
||||||
callback: Parameters<SdkActionManager["on"]>[1];
|
|
||||||
}) {
|
|
||||||
this.actionManager.off(action, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
preload({ calLink }: { calLink: string }) {
|
|
||||||
// eslint-disable-next-line prefer-rest-params
|
|
||||||
validate(arguments[0], {
|
|
||||||
required: true,
|
|
||||||
props: {
|
|
||||||
calLink: {
|
|
||||||
type: "string",
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const iframe = document.body.appendChild(document.createElement("iframe"));
|
|
||||||
const config = this.getConfig();
|
|
||||||
|
|
||||||
const urlInstance = new URL(`${config.origin}/${calLink}`);
|
|
||||||
urlInstance.searchParams.set("prerender", "true");
|
|
||||||
iframe.src = urlInstance.toString();
|
|
||||||
iframe.style.width = "0";
|
|
||||||
iframe.style.height = "0";
|
|
||||||
iframe.style.display = "none";
|
|
||||||
}
|
|
||||||
|
|
||||||
ui(uiConfig: UiConfig) {
|
|
||||||
validate(uiConfig, {
|
|
||||||
required: true,
|
|
||||||
props: {
|
|
||||||
theme: {
|
|
||||||
required: false,
|
|
||||||
type: "string",
|
|
||||||
},
|
|
||||||
styles: {
|
|
||||||
required: false,
|
|
||||||
type: Object,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
this.doInIframe({ method: "ui", arg: uiConfig });
|
|
||||||
}
|
|
||||||
|
|
||||||
doInIframe({
|
|
||||||
method,
|
|
||||||
arg,
|
|
||||||
}: // TODO: Need some TypeScript magic here to remove hardcoded types
|
|
||||||
| { method: "ui"; arg: Parameters<(typeof methods)["ui"]>[0] }
|
|
||||||
| { method: "parentKnowsIframeReady"; arg: undefined }) {
|
|
||||||
if (!this.iframeReady) {
|
if (!this.iframeReady) {
|
||||||
this.iframeDoQueue.push({ method, arg });
|
this.iframeDoQueue.push(doInIframeArg);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// TODO: Ensure that origin is as defined by user. Generally it would be cal.com but in case of self hosting it can be anything.
|
if (!this.iframe) {
|
||||||
this.iframe!.contentWindow!.postMessage({ originator: "CAL", method, arg }, "*");
|
throw new Error("iframe doesn't exist. `createIframe` must be called before `doInIframe`");
|
||||||
|
}
|
||||||
|
if (this.iframe.contentWindow) {
|
||||||
|
// TODO: Ensure that targetOrigin is as defined by user(and not *). Generally it would be cal.com but in case of self hosting it can be anything.
|
||||||
|
// Maybe we can derive targetOrigin from __config.origin
|
||||||
|
this.iframe.contentWindow.postMessage(
|
||||||
|
{ originator: "CAL", method: doInIframeArg.method, arg: doInIframeArg.arg },
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(namespace: string, q: InstructionQueue) {
|
constructor(namespace: string, q: IArguments[]) {
|
||||||
this.__config = {
|
this.__config = {
|
||||||
// Use WEBAPP_URL till full page reload problem with website URL is solved
|
// Use WEBAPP_URL till full page reload problem with website URL is solved
|
||||||
origin: WEBAPP_URL,
|
calOrigin: WEBAPP_URL,
|
||||||
};
|
};
|
||||||
|
this.api = new CalApi(this);
|
||||||
this.namespace = namespace;
|
this.namespace = namespace;
|
||||||
this.actionManager = new SdkActionManager(namespace);
|
this.actionManager = new SdkActionManager(namespace);
|
||||||
|
|
||||||
@@ -462,24 +299,17 @@ export class Cal {
|
|||||||
// 3. Parent window would check what iframe height can be set according to parent Element
|
// 3. Parent window would check what iframe height can be set according to parent Element
|
||||||
this.actionManager.on("__dimensionChanged", (e) => {
|
this.actionManager.on("__dimensionChanged", (e) => {
|
||||||
const { data } = e.detail;
|
const { data } = e.detail;
|
||||||
const iframe = this.iframe!;
|
const iframe = this.iframe;
|
||||||
|
|
||||||
if (!iframe) {
|
if (!iframe) {
|
||||||
// Iframe might be pre-rendering
|
// Iframe might be pre-rendering
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let unit = "px";
|
const unit = "px";
|
||||||
if (data.__unit) {
|
|
||||||
unit = data.__unit;
|
|
||||||
}
|
|
||||||
if (data.iframeHeight) {
|
if (data.iframeHeight) {
|
||||||
iframe.style.height = data.iframeHeight + unit;
|
iframe.style.height = data.iframeHeight + unit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if (data.iframeWidth) {
|
|
||||||
// iframe.style.width = data.iframeWidth + unit;
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (this.modalBox) {
|
if (this.modalBox) {
|
||||||
// It ensures that if the iframe is so tall that it can't fit in the parent window without scroll. Then force the scroll by restricting the max-height to innerHeight
|
// It ensures that if the iframe is so tall that it can't fit in the parent window without scroll. Then force the scroll by restricting the max-height to innerHeight
|
||||||
// This case is reproducible when viewing in ModalBox on Mobile.
|
// This case is reproducible when viewing in ModalBox on Mobile.
|
||||||
@@ -490,9 +320,9 @@ export class Cal {
|
|||||||
|
|
||||||
this.actionManager.on("__iframeReady", () => {
|
this.actionManager.on("__iframeReady", () => {
|
||||||
this.iframeReady = true;
|
this.iframeReady = true;
|
||||||
this.doInIframe({ method: "parentKnowsIframeReady", arg: undefined });
|
this.doInIframe({ method: "parentKnowsIframeReady" } as const);
|
||||||
this.iframeDoQueue.forEach(({ method, arg }) => {
|
this.iframeDoQueue.forEach((doInIframeArg) => {
|
||||||
this.doInIframe({ method, arg });
|
this.doInIframe(doInIframeArg);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -523,34 +353,276 @@ export class Cal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GlobalCal {
|
class CalApi {
|
||||||
(methodName: string, arg?: any): void;
|
cal: Cal;
|
||||||
|
|
||||||
|
constructor(cal: Cal) {
|
||||||
|
this.cal = cal;
|
||||||
|
}
|
||||||
|
|
||||||
|
init(namespaceOrConfig?: string | InitArgConfig, config = {} as InitArgConfig) {
|
||||||
|
if (typeof namespaceOrConfig !== "string") {
|
||||||
|
config = (namespaceOrConfig || {}) as Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { calOrigin: calOrigin, origin: origin, ...restConfig } = config;
|
||||||
|
|
||||||
|
this.cal.__config.calOrigin = calOrigin || origin || this.cal.__config.calOrigin;
|
||||||
|
|
||||||
|
this.cal.__config = { ...this.cal.__config, ...restConfig };
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* It is an instruction that adds embed iframe inline as last child of the element
|
||||||
|
*/
|
||||||
|
inline({
|
||||||
|
calLink,
|
||||||
|
elementOrSelector,
|
||||||
|
config,
|
||||||
|
}: {
|
||||||
|
calLink: string;
|
||||||
|
elementOrSelector: string | HTMLElement;
|
||||||
|
config?: PrefillAndIframeAttrsConfig;
|
||||||
|
}) {
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
validate(arguments[0], {
|
||||||
|
required: true,
|
||||||
|
props: {
|
||||||
|
calLink: {
|
||||||
|
// TODO: Add a special type calLink for it and validate that it doesn't start with / or https?://
|
||||||
|
required: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
elementOrSelector: {
|
||||||
|
required: true,
|
||||||
|
type: ["string", HTMLElement],
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
required: false,
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
config = config || {};
|
||||||
|
if (typeof config.iframeAttrs === "string" || config.iframeAttrs instanceof Array) {
|
||||||
|
throw new Error("iframeAttrs should be an object");
|
||||||
|
}
|
||||||
|
config.embedType = "inline";
|
||||||
|
const iframe = this.cal.createIframe({ calLink, queryObject: Cal.getQueryObject(config) });
|
||||||
|
iframe.style.height = "100%";
|
||||||
|
iframe.style.width = "100%";
|
||||||
|
const element =
|
||||||
|
elementOrSelector instanceof HTMLElement
|
||||||
|
? elementOrSelector
|
||||||
|
: document.querySelector(elementOrSelector);
|
||||||
|
if (!element) {
|
||||||
|
throw new Error("Element not found");
|
||||||
|
}
|
||||||
|
element.classList.add("cal-inline-container");
|
||||||
|
const template = document.createElement("template");
|
||||||
|
template.innerHTML = `<cal-inline style="max-height:inherit;height:inherit;min-height:inherit;display:flex;position:relative;flex-wrap:wrap;width:100%"></cal-inline><style>.cal-inline-container::-webkit-scrollbar{display:none}.cal-inline-container{scrollbar-width:none}</style>`;
|
||||||
|
this.cal.inlineEl = template.content.children[0];
|
||||||
|
this.cal.inlineEl.appendChild(iframe);
|
||||||
|
element.appendChild(template.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
floatingButton({
|
||||||
|
calLink,
|
||||||
|
buttonText = "Book my Cal",
|
||||||
|
hideButtonIcon = false,
|
||||||
|
attributes,
|
||||||
|
buttonPosition = "bottom-right",
|
||||||
|
buttonColor = "rgb(0, 0, 0)",
|
||||||
|
buttonTextColor = "rgb(255, 255, 255)",
|
||||||
|
calOrigin,
|
||||||
|
}: {
|
||||||
|
calLink: string;
|
||||||
|
buttonText?: string;
|
||||||
|
attributes?: Record<"id", string> & Record<string | "id", string>;
|
||||||
|
hideButtonIcon?: boolean;
|
||||||
|
buttonPosition?: "bottom-left" | "bottom-right";
|
||||||
|
buttonColor?: string;
|
||||||
|
buttonTextColor?: string;
|
||||||
|
calOrigin?: string;
|
||||||
|
}) {
|
||||||
|
// validate(arguments[0], {
|
||||||
|
// required: true,
|
||||||
|
// props: {
|
||||||
|
// calLink: {
|
||||||
|
// required: true,
|
||||||
|
// type: "string",
|
||||||
|
// },
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
let existingEl: HTMLElement | null = null;
|
||||||
|
|
||||||
|
if (attributes?.id) {
|
||||||
|
existingEl = document.getElementById(attributes.id);
|
||||||
|
}
|
||||||
|
let el: FloatingButton;
|
||||||
|
if (!existingEl) {
|
||||||
|
el = document.createElement("cal-floating-button") as FloatingButton;
|
||||||
|
// It makes it a target element that opens up embed modal on click
|
||||||
|
el.dataset.calLink = calLink;
|
||||||
|
el.dataset.calNamespace = this.cal.namespace;
|
||||||
|
el.dataset.calOrigin = calOrigin ?? "";
|
||||||
|
if (attributes?.id) {
|
||||||
|
el.id = attributes.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.body.appendChild(el);
|
||||||
|
} else {
|
||||||
|
el = existingEl as FloatingButton;
|
||||||
|
}
|
||||||
|
const dataset = el.dataset;
|
||||||
|
dataset["buttonText"] = buttonText;
|
||||||
|
dataset["hideButtonIcon"] = "" + hideButtonIcon;
|
||||||
|
dataset["buttonPosition"] = "" + buttonPosition;
|
||||||
|
dataset["buttonColor"] = "" + buttonColor;
|
||||||
|
dataset["buttonTextColor"] = "" + buttonTextColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
modal({
|
||||||
|
calLink,
|
||||||
|
calOrigin,
|
||||||
|
config = {},
|
||||||
|
uid,
|
||||||
|
}: {
|
||||||
|
calLink: string;
|
||||||
|
config?: PrefillAndIframeAttrsConfig;
|
||||||
|
uid?: string | number;
|
||||||
|
calOrigin?: string;
|
||||||
|
}) {
|
||||||
|
uid = uid || 0;
|
||||||
|
|
||||||
|
const existingModalEl = document.querySelector(`cal-modal-box[uid="${uid}"]`);
|
||||||
|
if (existingModalEl) {
|
||||||
|
existingModalEl.setAttribute("state", "started");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (typeof config.iframeAttrs === "string" || config.iframeAttrs instanceof Array) {
|
||||||
|
throw new Error("iframeAttrs should be an object");
|
||||||
|
}
|
||||||
|
config.embedType = "modal";
|
||||||
|
const iframe = this.cal.createIframe({ calLink, calOrigin, queryObject: Cal.getQueryObject(config) });
|
||||||
|
iframe.style.borderRadius = "8px";
|
||||||
|
|
||||||
|
iframe.style.height = "100%";
|
||||||
|
iframe.style.width = "100%";
|
||||||
|
const template = document.createElement("template");
|
||||||
|
template.innerHTML = `<cal-modal-box uid="${uid}"></cal-modal-box>`;
|
||||||
|
|
||||||
|
this.cal.modalBox = template.content.children[0];
|
||||||
|
this.cal.modalBox.appendChild(iframe);
|
||||||
|
this.cal.actionManager.on("__closeIframe", () => {
|
||||||
|
this.cal.modalBox.setAttribute("state", "closed");
|
||||||
|
});
|
||||||
|
document.body.appendChild(template.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
on<T extends keyof EventDataMap>({
|
||||||
|
action,
|
||||||
|
callback,
|
||||||
|
}: {
|
||||||
|
action: T;
|
||||||
|
callback: (arg0: CustomEvent<EventData<T>>) => void;
|
||||||
|
}) {
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
validate(arguments[0], {
|
||||||
|
required: true,
|
||||||
|
props: {
|
||||||
|
action: {
|
||||||
|
required: true,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
callback: {
|
||||||
|
required: true,
|
||||||
|
type: Function,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
this.cal.actionManager.on(action, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
off({ action, callback }: { action: never; callback: never }) {
|
||||||
|
this.cal.actionManager.off(action, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
preload({ calLink }: { calLink: string }) {
|
||||||
|
// eslint-disable-next-line prefer-rest-params
|
||||||
|
validate(arguments[0], {
|
||||||
|
required: true,
|
||||||
|
props: {
|
||||||
|
calLink: {
|
||||||
|
type: "string",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const iframe = document.body.appendChild(document.createElement("iframe"));
|
||||||
|
const config = this.cal.getConfig();
|
||||||
|
|
||||||
|
const urlInstance = new URL(`${config.calOrigin}/${calLink}`);
|
||||||
|
urlInstance.searchParams.set("prerender", "true");
|
||||||
|
iframe.src = urlInstance.toString();
|
||||||
|
iframe.style.width = "0";
|
||||||
|
iframe.style.height = "0";
|
||||||
|
iframe.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
ui(uiConfig: UiConfig) {
|
||||||
|
validate(uiConfig, {
|
||||||
|
required: true,
|
||||||
|
props: {
|
||||||
|
theme: {
|
||||||
|
required: false,
|
||||||
|
type: "string",
|
||||||
|
},
|
||||||
|
styles: {
|
||||||
|
required: false,
|
||||||
|
type: Object,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
this.cal.doInIframe({ method: "ui", arg: uiConfig });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is a full fledged Cal instance but doesn't have ns property because it would be nested inside an ns instance already
|
||||||
|
export interface GlobalCalWithoutNs {
|
||||||
|
<T extends keyof SingleInstructionMap>(methodName: T, ...arg: Rest<SingleInstructionMap[T]>): void;
|
||||||
/** Marks that the embed.js is loaded. Avoids re-downloading it. */
|
/** Marks that the embed.js is loaded. Avoids re-downloading it. */
|
||||||
loaded?: boolean;
|
loaded?: boolean;
|
||||||
/** Maintains a queue till the time embed.js isn't loaded */
|
/** Maintains a queue till the time embed.js isn't loaded */
|
||||||
q?: InstructionQueue;
|
q: IArguments[];
|
||||||
/** If user registers multiple namespaces, those are available here */
|
/** If user registers multiple namespaces, those are available here */
|
||||||
ns?: Record<string, GlobalCal>;
|
|
||||||
instance?: Cal;
|
instance?: Cal;
|
||||||
__css?: string;
|
__css?: string;
|
||||||
fingerprint?: string;
|
fingerprint?: string;
|
||||||
__logQueue?: any[];
|
__logQueue?: unknown[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Well Omit removes the Function Signature from a type if used. So, instead construct the types like this.
|
||||||
|
type GlobalCalWithNs = GlobalCalWithoutNs & {
|
||||||
|
ns: Record<string, GlobalCalWithoutNs>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GlobalCal = GlobalCalWithNs;
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
Cal?: GlobalCal;
|
Cal: GlobalCal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CalWindow extends Window {
|
export interface CalWindow extends Window {
|
||||||
Cal?: GlobalCal;
|
Cal: GlobalCal;
|
||||||
}
|
}
|
||||||
|
|
||||||
globalCal.instance = new Cal("", globalCal.q!);
|
globalCal.instance = new Cal("", globalCal.q);
|
||||||
|
for (const [ns, api] of Object.entries(globalCal.ns)) {
|
||||||
for (const [ns, api] of Object.entries(globalCal.ns!)) {
|
api.instance = new Cal(ns, api.q);
|
||||||
api.instance = new Cal(ns, api.q!);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -571,38 +643,45 @@ window.addEventListener("message", (e) => {
|
|||||||
if (!actionManager) {
|
if (!actionManager) {
|
||||||
throw new Error("Unhandled Action" + parsedAction);
|
throw new Error("Unhandled Action" + parsedAction);
|
||||||
}
|
}
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
actionManager.fire(parsedAction.type, detail.data);
|
actionManager.fire(parsedAction.type, detail.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
document.addEventListener("click", (e) => {
|
document.addEventListener("click", (e) => {
|
||||||
const htmlElement = e.target;
|
const targetEl = e.target;
|
||||||
if (!(htmlElement instanceof HTMLElement)) {
|
if (!(targetEl instanceof HTMLElement)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const path = htmlElement.dataset.calLink;
|
const path = targetEl.dataset.calLink;
|
||||||
if (!path) {
|
if (!path) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const modalUniqueId = ((htmlElement as unknown as any).uniqueId =
|
const modalUniqueId = (targetEl.dataset.uniqueId = targetEl.dataset.uniqueId || String(Date.now()));
|
||||||
(htmlElement as unknown as any).uniqueId || Date.now());
|
const namespace = targetEl.dataset.calNamespace;
|
||||||
const namespace = htmlElement.dataset.calNamespace;
|
const configString = targetEl.dataset.calConfig || "";
|
||||||
const configString = htmlElement.dataset.calConfig || "";
|
const calOrigin = targetEl.dataset.calOrigin || "";
|
||||||
let config;
|
let config;
|
||||||
try {
|
try {
|
||||||
config = JSON.parse(configString);
|
config = JSON.parse(configString);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
config = {};
|
config = {};
|
||||||
}
|
}
|
||||||
let api = globalCal;
|
|
||||||
|
let api: GlobalCalWithoutNs = globalCal;
|
||||||
|
|
||||||
if (namespace) {
|
if (namespace) {
|
||||||
api = globalCal.ns![namespace];
|
api = globalCal.ns[namespace];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!api) {
|
if (!api) {
|
||||||
throw new Error(`Namespace ${namespace} isn't defined`);
|
throw new Error(`Namespace ${namespace} isn't defined`);
|
||||||
}
|
}
|
||||||
|
|
||||||
api("modal", {
|
api("modal", {
|
||||||
calLink: path,
|
calLink: path,
|
||||||
config,
|
config,
|
||||||
uid: modalUniqueId,
|
uid: modalUniqueId,
|
||||||
|
calOrigin,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2,10 +2,9 @@ const WEBAPP_URL =
|
|||||||
import.meta.env.EMBED_PUBLIC_WEBAPP_URL || `https://${import.meta.env.EMBED_PUBLIC_VERCEL_URL}`;
|
import.meta.env.EMBED_PUBLIC_WEBAPP_URL || `https://${import.meta.env.EMBED_PUBLIC_VERCEL_URL}`;
|
||||||
const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
|
const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
|
||||||
|
|
||||||
(window as any).fingerprint = import.meta.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as string;
|
|
||||||
|
|
||||||
// Install Cal Embed Code Snippet
|
// Install Cal Embed Code Snippet
|
||||||
(function (C, A, L) {
|
(function (C, A, L) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const p = function (a: any, ar: any) {
|
const p = function (a: any, ar: any) {
|
||||||
a.q.push(ar);
|
a.q.push(ar);
|
||||||
};
|
};
|
||||||
@@ -13,7 +12,7 @@ const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_UR
|
|||||||
C.Cal =
|
C.Cal =
|
||||||
C.Cal ||
|
C.Cal ||
|
||||||
function () {
|
function () {
|
||||||
const cal = C.Cal!;
|
const cal = C.Cal;
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
const ar = arguments;
|
const ar = arguments;
|
||||||
if (!cal.loaded) {
|
if (!cal.loaded) {
|
||||||
@@ -23,13 +22,16 @@ const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_UR
|
|||||||
cal.loaded = true;
|
cal.loaded = true;
|
||||||
}
|
}
|
||||||
if (ar[0] === L) {
|
if (ar[0] === L) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const api: { (): void; q?: any[] } = function () {
|
const api: { (): void; q?: any[] } = function () {
|
||||||
// eslint-disable-next-line prefer-rest-params
|
// eslint-disable-next-line prefer-rest-params
|
||||||
p(api, arguments);
|
p(api, arguments);
|
||||||
};
|
};
|
||||||
const namespace = ar[1];
|
const namespace = ar[1];
|
||||||
api!.q = api.q || [];
|
api.q = api.q || [];
|
||||||
typeof namespace === "string" ? (cal!.ns![namespace] = api) && p(api, ar) : p(cal, ar);
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
//@ts-ignore
|
||||||
|
typeof namespace === "string" ? (cal.ns[namespace] = api) && p(api, ar) : p(cal, ar);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
p(cal, ar);
|
p(cal, ar);
|
||||||
@@ -37,20 +39,24 @@ const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_UR
|
|||||||
})(window, EMBED_LIB_URL, "init");
|
})(window, EMBED_LIB_URL, "init");
|
||||||
|
|
||||||
const previewWindow = window;
|
const previewWindow = window;
|
||||||
|
previewWindow.Cal.fingerprint = import.meta.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as string;
|
||||||
|
|
||||||
previewWindow.Cal!("init", {
|
previewWindow.Cal("init", {
|
||||||
origin: WEBAPP_URL,
|
origin: WEBAPP_URL,
|
||||||
});
|
});
|
||||||
const searchParams = new URL(document.URL).searchParams;
|
const searchParams = new URL(document.URL).searchParams;
|
||||||
const embedType = searchParams.get("embedType");
|
const embedType = searchParams.get("embedType");
|
||||||
const calLink = searchParams.get("calLink");
|
const calLink = searchParams.get("calLink");
|
||||||
if (embedType! === "inline") {
|
if (!calLink) {
|
||||||
previewWindow.Cal!("inline", {
|
throw new Error('Missing "calLink" query parameter');
|
||||||
|
}
|
||||||
|
if (embedType === "inline") {
|
||||||
|
previewWindow.Cal("inline", {
|
||||||
elementOrSelector: "#my-embed",
|
elementOrSelector: "#my-embed",
|
||||||
calLink: calLink,
|
calLink: calLink,
|
||||||
});
|
});
|
||||||
} else if (embedType === "floating-popup") {
|
} else if (embedType === "floating-popup") {
|
||||||
previewWindow.Cal!("floatingButton", {
|
previewWindow.Cal("floatingButton", {
|
||||||
calLink: calLink,
|
calLink: calLink,
|
||||||
attributes: {
|
attributes: {
|
||||||
id: "my-floating-button",
|
id: "my-floating-button",
|
||||||
@@ -58,7 +64,7 @@ if (embedType! === "inline") {
|
|||||||
});
|
});
|
||||||
} else if (embedType === "element-click") {
|
} else if (embedType === "element-click") {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.setAttribute("data-cal-link", calLink!);
|
button.setAttribute("data-cal-link", calLink);
|
||||||
button.innerHTML = "I am a button that exists on your website";
|
button.innerHTML = "I am a button that exists on your website";
|
||||||
document.body.appendChild(button);
|
document.body.appendChild(button);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
type Namespace = string;
|
type Namespace = string;
|
||||||
type CustomEventDetail = Record<string, any>;
|
type CustomEventDetail = Record<string, unknown>;
|
||||||
|
|
||||||
function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
||||||
const event = new window.CustomEvent(fullName, {
|
const event = new window.CustomEvent(fullName, {
|
||||||
@@ -9,6 +9,53 @@ function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
|||||||
window.dispatchEvent(event);
|
window.dispatchEvent(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EventDataMap = {
|
||||||
|
eventTypeSelected: {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
eventType: any;
|
||||||
|
};
|
||||||
|
linkFailed: {
|
||||||
|
code: string;
|
||||||
|
msg: string;
|
||||||
|
data: {
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
linkReady: Record<string, never>;
|
||||||
|
bookingSuccessful: {
|
||||||
|
// TODO: Shouldn't send the entire booking and eventType objects, we should send specific fields from them.
|
||||||
|
booking: unknown;
|
||||||
|
eventType: unknown;
|
||||||
|
date: string;
|
||||||
|
duration: number | undefined;
|
||||||
|
organizer: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
timeZone: string;
|
||||||
|
};
|
||||||
|
confirmed: boolean;
|
||||||
|
};
|
||||||
|
"*": Record<string, unknown>;
|
||||||
|
__routeChanged: Record<string, never>;
|
||||||
|
__windowLoadComplete: Record<string, never>;
|
||||||
|
__closeIframe: Record<string, never>;
|
||||||
|
__iframeReady: Record<string, never>;
|
||||||
|
__dimensionChanged: {
|
||||||
|
iframeHeight: number;
|
||||||
|
iframeWidth: number;
|
||||||
|
isFirstTime: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type EventData<T extends keyof EventDataMap> = {
|
||||||
|
[K in T]: {
|
||||||
|
type: string;
|
||||||
|
namespace: string;
|
||||||
|
fullType: string;
|
||||||
|
data: EventDataMap[K];
|
||||||
|
};
|
||||||
|
}[T];
|
||||||
|
|
||||||
export class SdkActionManager {
|
export class SdkActionManager {
|
||||||
namespace: Namespace;
|
namespace: Namespace;
|
||||||
|
|
||||||
@@ -31,7 +78,7 @@ export class SdkActionManager {
|
|||||||
return this.namespace ? `CAL:${this.namespace}:${name}` : `CAL::${name}`;
|
return this.namespace ? `CAL:${this.namespace}:${name}` : `CAL::${name}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
fire(name: string, data: CustomEventDetail) {
|
fire<T extends keyof EventDataMap>(name: T, data: EventDataMap[T]) {
|
||||||
const fullName = this.getFullActionName(name);
|
const fullName = this.getFullActionName(name);
|
||||||
const detail = {
|
const detail = {
|
||||||
type: name,
|
type: name,
|
||||||
@@ -46,12 +93,12 @@ export class SdkActionManager {
|
|||||||
_fireEvent(this.getFullActionName("*"), detail);
|
_fireEvent(this.getFullActionName("*"), detail);
|
||||||
}
|
}
|
||||||
|
|
||||||
on(name: string, callback: (arg0: CustomEvent<CustomEventDetail>) => void) {
|
on<T extends keyof EventDataMap>(name: T, callback: (arg0: CustomEvent<EventData<T>>) => void) {
|
||||||
const fullName = this.getFullActionName(name);
|
const fullName = this.getFullActionName(name);
|
||||||
window.addEventListener(fullName, callback as EventListener);
|
window.addEventListener(fullName, callback as EventListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
off(name: string, callback: (arg0: CustomEvent<CustomEventDetail>) => void) {
|
off<T extends keyof EventDataMap>(name: T, callback: (arg0: CustomEvent<EventData<T>>) => void) {
|
||||||
const fullName = this.getFullActionName(name);
|
const fullName = this.getFullActionName(name);
|
||||||
window.removeEventListener(fullName, callback as EventListener);
|
window.removeEventListener(fullName, callback as EventListener);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
{
|
{
|
||||||
"extends": "@calcom/tsconfig/base.json",
|
"extends": "@calcom/tsconfig/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"target": "ES2015",
|
||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"paths": {
|
|
||||||
"@lib/*": ["../../../apps/web/lib/*"]
|
|
||||||
},
|
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"baseUrl": "."
|
"baseUrl": ".",
|
||||||
|
"outDir": "dist",
|
||||||
|
"paths": {
|
||||||
|
"@calcom/embed-react": ["../embed-react/src"],
|
||||||
|
"@calcom/embed-snippet": ["../embed-snippet/src"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "env.d.ts", "index.ts"],
|
"include": ["src", "env.d.ts", "index.ts"],
|
||||||
"exclude": ["dist", "build", "node_modules"]
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
|
||||||
|
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
|
||||||
|
<script type="module" src="./floating.tsx"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/* First make sure that you have installed the package */
|
||||||
|
|
||||||
|
/* If you are using yarn */
|
||||||
|
// yarn add @calcom/embed-react
|
||||||
|
|
||||||
|
/* If you are using npm */
|
||||||
|
// npm install @calcom/embed-react
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import * as React from "react";
|
||||||
|
import ReactDom from "react-dom";
|
||||||
|
|
||||||
|
import { getCalApi } from "./src/index";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
useEffect(() => {
|
||||||
|
(async function () {
|
||||||
|
const cal = await getCalApi("http://localhost:3000/embed/embed.js");
|
||||||
|
cal("floatingButton", { calLink: "teampro/abc", calOrigin: "http://localhost:3000" });
|
||||||
|
cal("ui", { styles: { branding: { brandColor: "#000000" } }, hideEventTypeDetails: false });
|
||||||
|
})();
|
||||||
|
}, []);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDom.render(<App />, document.getElementById("root"));
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
|
<script src="https://unpkg.com/react@17/umd/react.development.js" crossorigin></script>
|
||||||
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
|
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js" crossorigin></script>
|
||||||
<script type="module" src="./test-cal.tsx"></script>
|
<script type="module" src="./inline.tsx"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import * as React from "react";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import ReactDom from "react-dom";
|
import ReactDom from "react-dom";
|
||||||
@@ -47,7 +48,6 @@ function App() {
|
|||||||
theme: "dark",
|
theme: "dark",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<button data-cal-link="pro">Popup</button>
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,7 @@
|
|||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"prepare": "NEXT_PUBLIC_EMBED_LIB_URL='https://app.cal.com/embed/embed.js' NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn build",
|
"prepare": "NEXT_PUBLIC_EMBED_LIB_URL='https://app.cal.com/embed/embed.js' NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn build",
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||||
"lint": "eslint --ext .ts,.js,.tsx,.jsx ./src",
|
"lint": "eslint --ext .ts,.js,.tsx,.jsx ./src",
|
||||||
"embed-tests": "yarn playwright test --config=./playwright/config/playwright.config.ts",
|
"embed-tests": "yarn playwright test --config=./playwright/config/playwright.config.ts",
|
||||||
"embed-tests-quick": "QUICK=true yarn embed-tests",
|
"embed-tests-quick": "QUICK=true yarn embed-tests",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable prefer-const */
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
|
|
||||||
import useEmbed from "./useEmbed";
|
import useEmbed from "./useEmbed";
|
||||||
@@ -10,7 +11,7 @@ type CalProps = {
|
|||||||
debug?: boolean;
|
debug?: boolean;
|
||||||
uiDebug?: boolean;
|
uiDebug?: boolean;
|
||||||
};
|
};
|
||||||
config?: any;
|
config?: Record<string, string>;
|
||||||
embedJsUrl?: string;
|
embedJsUrl?: string;
|
||||||
} & React.HTMLAttributes<HTMLDivElement>;
|
} & React.HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
@@ -23,7 +24,7 @@ const Cal = function Cal(props: CalProps) {
|
|||||||
const Cal = useEmbed(embedJsUrl);
|
const Cal = useEmbed(embedJsUrl);
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!Cal || initializedRef.current) {
|
if (!Cal || initializedRef.current || !ref.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
initializedRef.current = true;
|
initializedRef.current = true;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import type { GlobalCal } from "@calcom/embed-core";
|
import type { GlobalCal } from "@calcom/embed-core";
|
||||||
import EmbedSnippet from "@calcom/embed-snippet";
|
import EmbedSnippet from "@calcom/embed-snippet";
|
||||||
|
|
||||||
import Cal from "./Cal";
|
import Cal from "./Cal";
|
||||||
|
|
||||||
export const getCalApi = (): Promise<GlobalCal> =>
|
export const getCalApi = (embedJsUrl?: string): Promise<GlobalCal> =>
|
||||||
new Promise(function tryReadingFromWindow(resolve) {
|
new Promise(function tryReadingFromWindow(resolve) {
|
||||||
EmbedSnippet();
|
EmbedSnippet(embedJsUrl);
|
||||||
const api = window.Cal;
|
const api = window.Cal;
|
||||||
if (!api) {
|
if (!api) {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
import EmbedSnippet from "@calcom/embed-snippet";
|
import EmbedSnippet from "@calcom/embed-snippet";
|
||||||
|
|||||||
@@ -2,12 +2,15 @@
|
|||||||
"extends": "@calcom/tsconfig/base.json",
|
"extends": "@calcom/tsconfig/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
|
"target": "ES2015",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
|
"outDir": "dist",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@lib/*": ["../../../apps/web/lib/*"]
|
"@calcom/embed-core": ["../embed-core/src"],
|
||||||
|
"@calcom/embed-snippet": ["../embed-snippet/src"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["src", "env.d.ts"],
|
"include": ["src", "env.d.ts"],
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "vite build && tsc --emitDeclarationOnly --declarationDir dist",
|
"build": "vite build && tsc --emitDeclarationOnly --declarationDir dist",
|
||||||
"type-check": "tsc --pretty --noEmit",
|
"type-check": "tsc --pretty --noEmit",
|
||||||
|
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||||
"lint": "eslint --ext .ts,.js src",
|
"lint": "eslint --ext .ts,.js src",
|
||||||
"prepare": "NEXT_PUBLIC_EMBED_LIB_URL='https://app.cal.com/embed/embed.js' NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn build",
|
"prepare": "NEXT_PUBLIC_EMBED_LIB_URL='https://app.cal.com/embed/embed.js' NEXT_PUBLIC_WEBAPP_URL='https://app.cal.com' yarn build",
|
||||||
"publish-prod:patch": "yarn publish --access public --patch",
|
"publish-prod:patch": "yarn publish --access public --patch",
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-ts-comment,prefer-rest-params,prefer-const */
|
/* eslint-disable @typescript-eslint/ban-ts-comment,prefer-rest-params,prefer-const */
|
||||||
|
import type { GlobalCal, GlobalCalWithoutNs } from "@calcom/embed-core";
|
||||||
|
import type { Optional } from "@calcom/types/utils";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* As we want to keep control on the size of this snippet but we want some portion of it to be still readable.
|
* As we want to keep control on the size of this snippet but we want some portion of it to be still readable.
|
||||||
* So, write the code that you need directly but keep it short.
|
* So, write the code that you need directly but keep it short.
|
||||||
*/
|
*/
|
||||||
import type { CalWindow } from "@calcom/embed-core";
|
|
||||||
|
|
||||||
const WEBAPP_URL =
|
const WEBAPP_URL =
|
||||||
import.meta.env.EMBED_PUBLIC_WEBAPP_URL || `https://${import.meta.env.EMBED_PUBLIC_VERCEL_URL}`;
|
import.meta.env.EMBED_PUBLIC_WEBAPP_URL || `https://${import.meta.env.EMBED_PUBLIC_VERCEL_URL}`;
|
||||||
@@ -12,37 +13,45 @@ const WEBAPP_URL =
|
|||||||
const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
|
const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_URL}/embed/embed.js`;
|
||||||
|
|
||||||
export default function EmbedSnippet(url = EMBED_LIB_URL) {
|
export default function EmbedSnippet(url = EMBED_LIB_URL) {
|
||||||
(function (C: CalWindow, A, L) {
|
(function (C, A, L) {
|
||||||
let p = function (a: any, ar: any) {
|
let p = function (a: GlobalCalWithoutNs, ar: IArguments) {
|
||||||
a.q.push(ar);
|
a.q.push(ar);
|
||||||
};
|
};
|
||||||
let d = C.document;
|
let d = C.document;
|
||||||
C.Cal =
|
C.Cal =
|
||||||
C.Cal ||
|
C.Cal ||
|
||||||
function () {
|
function () {
|
||||||
let cal = C.Cal!;
|
let cal = C.Cal;
|
||||||
let ar = arguments;
|
let ar = arguments;
|
||||||
if (!cal.loaded) {
|
if (!cal.loaded) {
|
||||||
|
// 'ns' and 'q' are now definitely set with the following 2 lines, so you can safely assert in TypeScript that it's GlobalCal now.
|
||||||
cal.ns = {};
|
cal.ns = {};
|
||||||
cal.q = cal.q || [];
|
cal.q = cal.q || [];
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
d.head.appendChild(d.createElement("script")).src = A;
|
d.head.appendChild(d.createElement("script")).src = A;
|
||||||
cal.loaded = true;
|
cal.loaded = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ar[0] === L) {
|
if (ar[0] === L) {
|
||||||
const api: { (): void; q: any[] } = function () {
|
const api: GlobalCalWithoutNs = function () {
|
||||||
p(api, arguments);
|
p(api, arguments);
|
||||||
};
|
};
|
||||||
const namespace = ar[1];
|
const namespace = ar[1];
|
||||||
api.q = api.q || [];
|
api.q = api.q || [];
|
||||||
typeof namespace === "string" ? (cal.ns![namespace] = api) && p(api, ar) : p(cal, ar);
|
typeof namespace === "string"
|
||||||
|
? // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
(cal.ns![namespace] = api) && p(api, ar)
|
||||||
|
: p(cal as GlobalCal, ar);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
p(cal, ar);
|
p(cal as GlobalCal, ar);
|
||||||
};
|
};
|
||||||
})(
|
})(
|
||||||
window,
|
window as Omit<Window, "Cal"> & {
|
||||||
|
// Make 'ns' and 'q' optional as they are set through the snippet above
|
||||||
|
Cal: Optional<GlobalCal, "ns" | "q">;
|
||||||
|
},
|
||||||
//! Replace it with "https://cal.com/embed.js" or the URL where you have embed.js installed
|
//! Replace it with "https://cal.com/embed.js" or the URL where you have embed.js installed
|
||||||
url,
|
url,
|
||||||
"init"
|
"init"
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
{
|
{
|
||||||
"extends": "@calcom/tsconfig/base.json",
|
"extends": "@calcom/tsconfig/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"target": "ES2015",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"module": "esnext",
|
"module": "ESNext",
|
||||||
"declaration": true
|
"declaration": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"paths": {
|
||||||
|
"@calcom/embed-core": ["../embed-core/src"],
|
||||||
|
"@calcom/embed-react": ["../embed-react/src"]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"include": ["."],
|
"include": ["."],
|
||||||
"exclude": ["dist", "build", "node_modules"]
|
"exclude": ["dist", "build", "node_modules"]
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ async function getBookingToDelete(id: number | undefined, uid: string | undefine
|
|||||||
currency: true,
|
currency: true,
|
||||||
length: true,
|
length: true,
|
||||||
seatsPerTimeSlot: true,
|
seatsPerTimeSlot: true,
|
||||||
|
seatsShowAttendees: true,
|
||||||
hosts: {
|
hosts: {
|
||||||
select: {
|
select: {
|
||||||
user: true,
|
user: true,
|
||||||
@@ -190,6 +191,8 @@ async function handler(req: CustomRequest) {
|
|||||||
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
|
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
|
||||||
cancellationReason: cancellationReason,
|
cancellationReason: cancellationReason,
|
||||||
...(teamMembers && { team: { name: "", members: teamMembers } }),
|
...(teamMembers && { team: { name: "", members: teamMembers } }),
|
||||||
|
seatsPerTimeSlot: bookingToDelete.eventType?.seatsPerTimeSlot,
|
||||||
|
seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees,
|
||||||
};
|
};
|
||||||
|
|
||||||
// If it's just an attendee of a booking then just remove them from that booking
|
// If it's just an attendee of a booking then just remove them from that booking
|
||||||
|
|||||||
@@ -622,6 +622,7 @@ async function handler(
|
|||||||
|
|
||||||
let locationBodyString = location;
|
let locationBodyString = location;
|
||||||
let defaultLocationUrl = undefined;
|
let defaultLocationUrl = undefined;
|
||||||
|
|
||||||
if (dynamicUserList.length > 1) {
|
if (dynamicUserList.length > 1) {
|
||||||
users = users.sort((a, b) => {
|
users = users.sort((a, b) => {
|
||||||
const aIndex = (a.username && dynamicUserList.indexOf(a.username)) || 0;
|
const aIndex = (a.username && dynamicUserList.indexOf(a.username)) || 0;
|
||||||
@@ -701,6 +702,18 @@ async function handler(
|
|||||||
|
|
||||||
const [organizerUser] = users;
|
const [organizerUser] = users;
|
||||||
const tOrganizer = await getTranslation(organizerUser?.locale ?? "en", "common");
|
const tOrganizer = await getTranslation(organizerUser?.locale ?? "en", "common");
|
||||||
|
// use host default
|
||||||
|
if (isTeamEventType && locationBodyString === "conferencing") {
|
||||||
|
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
|
||||||
|
const organizerMetadata = metadataParseResult.success ? metadataParseResult.data : undefined;
|
||||||
|
if (organizerMetadata) {
|
||||||
|
const app = getAppFromSlug(organizerMetadata?.defaultConferencingApp?.appSlug);
|
||||||
|
locationBodyString = app?.appData?.location?.type || locationBodyString;
|
||||||
|
defaultLocationUrl = organizerMetadata?.defaultConferencingApp?.appLink;
|
||||||
|
} else {
|
||||||
|
locationBodyString = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const invitee = [
|
const invitee = [
|
||||||
{
|
{
|
||||||
@@ -882,10 +895,13 @@ async function handler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Promise.all(integrationsToDelete).then(async () => {
|
await Promise.all(integrationsToDelete).then(async () => {
|
||||||
await prisma.booking.delete({
|
await prisma.booking.update({
|
||||||
where: {
|
where: {
|
||||||
id: originalRescheduledBooking.id,
|
id: originalRescheduledBooking.id,
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
|
status: BookingStatus.CANCELLED,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
deletedReferences = true;
|
deletedReferences = true;
|
||||||
@@ -997,6 +1013,16 @@ async function handler(
|
|||||||
throw new HttpError({ statusCode: 401 });
|
throw new HttpError({ statusCode: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Moving forward in this block is the owner making changes to the booking. All attendees should be affected
|
||||||
|
evt.attendees = originalRescheduledBooking.attendees.map((attendee) => {
|
||||||
|
return {
|
||||||
|
name: attendee.name,
|
||||||
|
email: attendee.email,
|
||||||
|
timeZone: attendee.timeZone,
|
||||||
|
language: { translate: tAttendees, locale: attendee.locale ?? "en" },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// If owner reschedules the event we want to update the entire booking
|
// If owner reschedules the event we want to update the entire booking
|
||||||
// Also if owner is rescheduling there should be no bookingSeat
|
// Also if owner is rescheduling there should be no bookingSeat
|
||||||
|
|
||||||
@@ -1031,6 +1057,10 @@ async function handler(
|
|||||||
|
|
||||||
const results = updateManager.results;
|
const results = updateManager.results;
|
||||||
|
|
||||||
|
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
||||||
|
|
||||||
|
evt.iCalUID = calendarResult.updatedEvent.iCalUID || undefined;
|
||||||
|
|
||||||
if (results.length > 0 && results.some((res) => !res.success)) {
|
if (results.length > 0 && results.some((res) => !res.success)) {
|
||||||
const error = {
|
const error = {
|
||||||
errorCode: "BookingReschedulingMeetingFailed",
|
errorCode: "BookingReschedulingMeetingFailed",
|
||||||
@@ -1058,7 +1088,7 @@ async function handler(
|
|||||||
await sendRescheduledEmails({
|
await sendRescheduledEmails({
|
||||||
...copyEvent,
|
...copyEvent,
|
||||||
additionalNotes, // Resets back to the additionalNote input and not the override value
|
additionalNotes, // Resets back to the additionalNote input and not the override value
|
||||||
cancellationReason: "$RCH$" + rescheduleReason ? rescheduleReason : "", // Removable code prefix to differentiate cancellation from rescheduling for email
|
cancellationReason: "$RCH$" + (rescheduleReason ? rescheduleReason : ""), // Removable code prefix to differentiate cancellation from rescheduling for email
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
const resultBooking = await resultBookingQuery(newBooking.id);
|
const resultBooking = await resultBookingQuery(newBooking.id);
|
||||||
@@ -1157,20 +1187,31 @@ async function handler(
|
|||||||
|
|
||||||
const copyEvent = cloneDeep(evt);
|
const copyEvent = cloneDeep(evt);
|
||||||
|
|
||||||
await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
|
const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
|
||||||
|
|
||||||
|
const results = updateManager.results;
|
||||||
|
|
||||||
|
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
||||||
|
|
||||||
|
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
|
||||||
|
? calendarResult?.updatedEvent[0]?.iCalUID
|
||||||
|
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
||||||
|
|
||||||
// TODO send reschedule emails to attendees of the old booking
|
// TODO send reschedule emails to attendees of the old booking
|
||||||
await sendRescheduledEmails({
|
await sendRescheduledEmails({
|
||||||
...copyEvent,
|
...copyEvent,
|
||||||
additionalNotes, // Resets back to the additionalNote input and not the override value
|
additionalNotes, // Resets back to the additionalNote input and not the override value
|
||||||
cancellationReason: "$RCH$" + rescheduleReason ? rescheduleReason : "", // Removable code prefix to differentiate cancellation from rescheduling for email
|
cancellationReason: "$RCH$" + (rescheduleReason ? rescheduleReason : ""), // Removable code prefix to differentiate cancellation from rescheduling for email
|
||||||
});
|
});
|
||||||
|
|
||||||
// Delete the old booking
|
// Update the old booking with the cancelled status
|
||||||
await prisma.booking.delete({
|
await prisma.booking.update({
|
||||||
where: {
|
where: {
|
||||||
id: booking.id,
|
id: booking.id,
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
|
status: BookingStatus.CANCELLED,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const resultBooking = await resultBookingQuery(newTimeSlotBooking.id);
|
const resultBooking = await resultBookingQuery(newTimeSlotBooking.id);
|
||||||
@@ -1237,7 +1278,15 @@ async function handler(
|
|||||||
|
|
||||||
const copyEvent = cloneDeep(evt);
|
const copyEvent = cloneDeep(evt);
|
||||||
|
|
||||||
await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
|
const updateManager = await eventManager.reschedule(copyEvent, rescheduleUid, newTimeSlotBooking.id);
|
||||||
|
|
||||||
|
const results = updateManager.results;
|
||||||
|
|
||||||
|
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
||||||
|
|
||||||
|
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
|
||||||
|
? calendarResult?.updatedEvent[0]?.iCalUID
|
||||||
|
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
||||||
|
|
||||||
await sendRescheduledSeatEmail(copyEvent, seatAttendee as Person);
|
await sendRescheduledSeatEmail(copyEvent, seatAttendee as Person);
|
||||||
const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => {
|
const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => {
|
||||||
@@ -1271,6 +1320,8 @@ async function handler(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const attendeeUniqueId = uuid();
|
||||||
|
|
||||||
const bookingUpdated = await prisma.booking.update({
|
const bookingUpdated = await prisma.booking.update({
|
||||||
where: {
|
where: {
|
||||||
uid: reqBody.bookingUid,
|
uid: reqBody.bookingUid,
|
||||||
@@ -1285,16 +1336,9 @@ async function handler(
|
|||||||
name: invitee[0].name,
|
name: invitee[0].name,
|
||||||
timeZone: invitee[0].timeZone,
|
timeZone: invitee[0].timeZone,
|
||||||
locale: invitee[0].language.locale,
|
locale: invitee[0].language.locale,
|
||||||
},
|
bookingSeat: {
|
||||||
},
|
create: {
|
||||||
...(booking.status === BookingStatus.CANCELLED && { status: BookingStatus.ACCEPTED }),
|
referenceUid: attendeeUniqueId,
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Add entry to bookingSeat table
|
|
||||||
const attendeeUniqueId = uuid();
|
|
||||||
await prisma.bookingSeat.create({
|
|
||||||
data: {
|
|
||||||
data: {
|
data: {
|
||||||
description: additionalNotes,
|
description: additionalNotes,
|
||||||
},
|
},
|
||||||
@@ -1303,14 +1347,14 @@ async function handler(
|
|||||||
id: booking.id,
|
id: booking.id,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
referenceUid: attendeeUniqueId,
|
|
||||||
attendee: {
|
|
||||||
connect: {
|
|
||||||
id: bookingUpdated.attendees[bookingUpdated.attendees.length - 1].id,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
...(booking.status === BookingStatus.CANCELLED && { status: BookingStatus.ACCEPTED }),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
evt.attendeeSeatId = attendeeUniqueId;
|
evt.attendeeSeatId = attendeeUniqueId;
|
||||||
|
|
||||||
const newSeat = booking.attendees.length !== 0;
|
const newSeat = booking.attendees.length !== 0;
|
||||||
@@ -1495,6 +1539,9 @@ async function handler(
|
|||||||
};
|
};
|
||||||
newBookingData["paid"] = originalRescheduledBooking.paid;
|
newBookingData["paid"] = originalRescheduledBooking.paid;
|
||||||
newBookingData["fromReschedule"] = originalRescheduledBooking.uid;
|
newBookingData["fromReschedule"] = originalRescheduledBooking.uid;
|
||||||
|
if (originalRescheduledBooking.uid) {
|
||||||
|
newBookingData.cancellationReason = rescheduleReason;
|
||||||
|
}
|
||||||
if (newBookingData.attendees?.createMany?.data) {
|
if (newBookingData.attendees?.createMany?.data) {
|
||||||
// Reschedule logic with booking with seats
|
// Reschedule logic with booking with seats
|
||||||
if (eventType?.seatsPerTimeSlot && bookerEmail) {
|
if (eventType?.seatsPerTimeSlot && bookerEmail) {
|
||||||
@@ -1548,7 +1595,7 @@ async function handler(
|
|||||||
return prisma.booking.create(createBookingObj);
|
return prisma.booking.create(createBookingObj);
|
||||||
}
|
}
|
||||||
|
|
||||||
let results: EventResult<AdditionalInformation & { url?: string }>[] = [];
|
let results: EventResult<AdditionalInformation & { url?: string; iCalUID?: string }>[] = [];
|
||||||
let referencesToCreate: PartialReference[] = [];
|
let referencesToCreate: PartialReference[] = [];
|
||||||
|
|
||||||
type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
type Booking = Prisma.PromiseReturnType<typeof createBooking>;
|
||||||
@@ -1662,12 +1709,15 @@ async function handler(
|
|||||||
addVideoCallDataToEvt(originalRescheduledBooking.references);
|
addVideoCallDataToEvt(originalRescheduledBooking.references);
|
||||||
const updateManager = await eventManager.reschedule(evt, originalRescheduledBooking.uid);
|
const updateManager = await eventManager.reschedule(evt, originalRescheduledBooking.uid);
|
||||||
|
|
||||||
//delete original rescheduled booking (no seats event)
|
//update original rescheduled booking (no seats event)
|
||||||
if (!eventType.seatsPerTimeSlot) {
|
if (!eventType.seatsPerTimeSlot) {
|
||||||
await prisma.booking.delete({
|
await prisma.booking.update({
|
||||||
where: {
|
where: {
|
||||||
id: originalRescheduledBooking.id,
|
id: originalRescheduledBooking.id,
|
||||||
},
|
},
|
||||||
|
data: {
|
||||||
|
status: BookingStatus.CANCELLED,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1686,6 +1736,11 @@ async function handler(
|
|||||||
log.error(`Booking ${organizerUser.name} failed`, error, results);
|
log.error(`Booking ${organizerUser.name} failed`, error, results);
|
||||||
} else {
|
} else {
|
||||||
const metadata: AdditionalInformation = {};
|
const metadata: AdditionalInformation = {};
|
||||||
|
const calendarResult = results.find((result) => result.type.includes("_calendar"));
|
||||||
|
|
||||||
|
evt.iCalUID = Array.isArray(calendarResult?.updatedEvent)
|
||||||
|
? calendarResult?.updatedEvent[0]?.iCalUID
|
||||||
|
: calendarResult?.updatedEvent?.iCalUID || undefined;
|
||||||
|
|
||||||
if (results.length) {
|
if (results.length) {
|
||||||
// TODO: Handle created event metadata more elegantly
|
// TODO: Handle created event metadata more elegantly
|
||||||
@@ -1706,7 +1761,7 @@ async function handler(
|
|||||||
...copyEvent,
|
...copyEvent,
|
||||||
additionalInformation: metadata,
|
additionalInformation: metadata,
|
||||||
additionalNotes, // Resets back to the additionalNote input and not the override value
|
additionalNotes, // Resets back to the additionalNote input and not the override value
|
||||||
cancellationReason: "$RCH$" + rescheduleReason ? rescheduleReason : "", // Removable code prefix to differentiate cancellation from rescheduling for email
|
cancellationReason: "$RCH$" + (rescheduleReason ? rescheduleReason : ""), // Removable code prefix to differentiate cancellation from rescheduling for email
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import MemberInvitationModal from "@calcom/features/ee/teams/components/MemberIn
|
|||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { WEBAPP_URL, APP_NAME } from "@calcom/lib/constants";
|
import { WEBAPP_URL, APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Avatar, Badge, Button, showToast, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
import { Avatar, Badge, Button, showToast, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||||
import { FiPlus, FiArrowRight, FiTrash2 } from "@calcom/ui/components/icon";
|
import { FiPlus, FiArrowRight, FiTrash2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
@@ -43,9 +44,17 @@ export const AddNewTeamMembersForm = ({
|
|||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
|
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
|
||||||
async onSuccess() {
|
async onSuccess(data) {
|
||||||
await utils.viewer.teams.get.invalidate();
|
await utils.viewer.teams.get.invalidate();
|
||||||
setMemberInviteModal(false);
|
setMemberInviteModal(false);
|
||||||
|
if (data.sendEmailInvitation) {
|
||||||
|
showToast(
|
||||||
|
t("email_invite_team", {
|
||||||
|
email: data.usernameOrEmail,
|
||||||
|
}),
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast(error.message, "error");
|
showToast(error.message, "error");
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
import { useState } from "react";
|
||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -7,7 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import slugify from "@calcom/lib/slugify";
|
import slugify from "@calcom/lib/slugify";
|
||||||
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Avatar, Button, Form, ImageUploader, TextField } from "@calcom/ui";
|
import { Avatar, Button, Form, ImageUploader, TextField, Alert } from "@calcom/ui";
|
||||||
import { FiArrowRight } from "@calcom/ui/components/icon";
|
import { FiArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { NewTeamFormValues } from "../lib/types";
|
import type { NewTeamFormValues } from "../lib/types";
|
||||||
@@ -21,6 +22,7 @@ export const CreateANewTeamForm = () => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const returnToParsed = querySchema.safeParse(router.query);
|
const returnToParsed = querySchema.safeParse(router.query);
|
||||||
|
const [serverErrorMessage, setServerErrorMessage] = useState<string | null>(null);
|
||||||
|
|
||||||
const returnToParam =
|
const returnToParam =
|
||||||
(returnToParsed.success ? getSafeRedirectUrl(returnToParsed.data.returnTo) : "/settings/teams") ||
|
(returnToParsed.success ? getSafeRedirectUrl(returnToParsed.data.returnTo) : "/settings/teams") ||
|
||||||
@@ -33,29 +35,32 @@ export const CreateANewTeamForm = () => {
|
|||||||
telemetry.event(telemetryEventTypes.team_created);
|
telemetry.event(telemetryEventTypes.team_created);
|
||||||
router.push(`/settings/teams/${data.id}/onboard-members`);
|
router.push(`/settings/teams/${data.id}/onboard-members`);
|
||||||
},
|
},
|
||||||
});
|
onError: (err) => {
|
||||||
|
if (err.message === "team_url_taken") {
|
||||||
const validateTeamSlugQuery = trpc.viewer.teams.validateTeamSlug.useQuery(
|
newTeamFormMethods.setError("slug", { type: "custom", message: t("team_url_taken") });
|
||||||
{ slug: newTeamFormMethods.watch("slug") },
|
} else {
|
||||||
{
|
setServerErrorMessage(err.message);
|
||||||
enabled: false,
|
|
||||||
refetchOnWindowFocus: false,
|
|
||||||
}
|
}
|
||||||
);
|
},
|
||||||
|
});
|
||||||
const validateTeamSlug = async () => {
|
|
||||||
await validateTeamSlugQuery.refetch();
|
|
||||||
if (validateTeamSlugQuery.isFetched) return validateTeamSlugQuery.data || t("team_url_taken");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Form
|
<Form
|
||||||
form={newTeamFormMethods}
|
form={newTeamFormMethods}
|
||||||
handleSubmit={(v) => {
|
handleSubmit={(v) => {
|
||||||
if (!createTeamMutation.isLoading) createTeamMutation.mutate(v);
|
if (!createTeamMutation.isLoading) {
|
||||||
|
setServerErrorMessage(null);
|
||||||
|
createTeamMutation.mutate(v);
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
|
{serverErrorMessage && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<Alert severity="error" message={serverErrorMessage} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="name"
|
name="name"
|
||||||
control={newTeamFormMethods.control}
|
control={newTeamFormMethods.control}
|
||||||
@@ -75,7 +80,6 @@ export const CreateANewTeamForm = () => {
|
|||||||
if (newTeamFormMethods.formState.touchedFields["slug"] === undefined) {
|
if (newTeamFormMethods.formState.touchedFields["slug"] === undefined) {
|
||||||
newTeamFormMethods.setValue("slug", slugify(e?.target.value));
|
newTeamFormMethods.setValue("slug", slugify(e?.target.value));
|
||||||
}
|
}
|
||||||
createTeamMutation.reset();
|
|
||||||
}}
|
}}
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
@@ -88,7 +92,7 @@ export const CreateANewTeamForm = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="slug"
|
name="slug"
|
||||||
control={newTeamFormMethods.control}
|
control={newTeamFormMethods.control}
|
||||||
rules={{ required: t("team_url_required"), validate: async () => await validateTeamSlug() }}
|
rules={{ required: t("team_url_required") }}
|
||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<TextField
|
<TextField
|
||||||
className="mt-2"
|
className="mt-2"
|
||||||
@@ -103,7 +107,7 @@ export const CreateANewTeamForm = () => {
|
|||||||
newTeamFormMethods.setValue("slug", slugify(e?.target.value), {
|
newTeamFormMethods.setValue("slug", slugify(e?.target.value), {
|
||||||
shouldTouch: true,
|
shouldTouch: true,
|
||||||
});
|
});
|
||||||
createTeamMutation.reset();
|
newTeamFormMethods.clearErrors("slug");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -143,11 +147,7 @@ export const CreateANewTeamForm = () => {
|
|||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={newTeamFormMethods.formState.isSubmitting || createTeamMutation.isLoading}
|
||||||
newTeamFormMethods.formState.isSubmitting ||
|
|
||||||
createTeamMutation.isError ||
|
|
||||||
createTeamMutation.isLoading
|
|
||||||
}
|
|
||||||
color="primary"
|
color="primary"
|
||||||
EndIcon={FiArrowRight}
|
EndIcon={FiArrowRight}
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -155,9 +155,6 @@ export const CreateANewTeamForm = () => {
|
|||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{createTeamMutation.isError && (
|
|
||||||
<p className="mt-4 text-red-700">{createTeamMutation.error.message}</p>
|
|
||||||
)}
|
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -93,6 +93,8 @@ export default function MemberListItem(props: Props) {
|
|||||||
props.member.accepted &&
|
props.member.accepted &&
|
||||||
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true";
|
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true";
|
||||||
|
|
||||||
|
const bookingLink = `${WEBAPP_URL.split("http://")[1]}/${props.member.username}`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li className="divide-y px-5">
|
<li className="divide-y px-5">
|
||||||
<div className="my-4 flex justify-between">
|
<div className="my-4 flex justify-between">
|
||||||
@@ -112,12 +114,20 @@ export default function MemberListItem(props: Props) {
|
|||||||
{!props.member.accepted && <TeamPill color="orange" text={t("pending")} />}
|
{!props.member.accepted && <TeamPill color="orange" text={t("pending")} />}
|
||||||
{props.member.role && <TeamRole role={props.member.role} />}
|
{props.member.role && <TeamRole role={props.member.role} />}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
<span
|
<span
|
||||||
className="block text-sm text-gray-600"
|
className="block text-sm text-gray-600"
|
||||||
data-testid="member-email"
|
data-testid="member-email"
|
||||||
data-email={props.member.email}>
|
data-email={props.member.email}>
|
||||||
{props.member.email}
|
{props.member.email}
|
||||||
</span>
|
</span>
|
||||||
|
{bookingLink && (
|
||||||
|
<>
|
||||||
|
<span className="mx-2 block text-gray-600">•</span>
|
||||||
|
<span className="block text-sm text-gray-600">{bookingLink}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export default function TeamList(props: Props) {
|
|||||||
<>
|
<>
|
||||||
{props.teams.map(
|
{props.teams.map(
|
||||||
(team, i) =>
|
(team, i) =>
|
||||||
|
team.role !== "MEMBER" &&
|
||||||
i === 0 && (
|
i === 0 && (
|
||||||
<div className="bg-gray-100 p-6">
|
<div className="bg-gray-100 p-6">
|
||||||
<h3 className="mb-4 text-sm font-semibold text-gray-900">{t("recommended_next_steps")}</h3>
|
<h3 className="mb-4 text-sm font-semibold text-gray-900">{t("recommended_next_steps")}</h3>
|
||||||
@@ -96,11 +97,7 @@ export default function TeamList(props: Props) {
|
|||||||
title={t("collective_or_roundrobin")}
|
title={t("collective_or_roundrobin")}
|
||||||
description={t("book_your_team_members")}
|
description={t("book_your_team_members")}
|
||||||
actionButton={{
|
actionButton={{
|
||||||
href:
|
href: "/event-types?dialog=new&eventPage=team%2F" + team.slug + "&teamId=" + team.id,
|
||||||
"/event-types?dialog=new-eventtype&eventPage=team%2F" +
|
|
||||||
team.slug +
|
|
||||||
"&teamId=" +
|
|
||||||
team.id,
|
|
||||||
child: t("create"),
|
child: t("create"),
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -56,9 +56,17 @@ export default function TeamListItem(props: Props) {
|
|||||||
const [openMemberInvitationModal, setOpenMemberInvitationModal] = useState(false);
|
const [openMemberInvitationModal, setOpenMemberInvitationModal] = useState(false);
|
||||||
const teamQuery = trpc.viewer.teams.get.useQuery({ teamId: team?.id });
|
const teamQuery = trpc.viewer.teams.get.useQuery({ teamId: team?.id });
|
||||||
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
|
const inviteMemberMutation = trpc.viewer.teams.inviteMember.useMutation({
|
||||||
async onSuccess() {
|
async onSuccess(data) {
|
||||||
await utils.viewer.teams.get.invalidate();
|
await utils.viewer.teams.get.invalidate();
|
||||||
setOpenMemberInvitationModal(false);
|
setOpenMemberInvitationModal(false);
|
||||||
|
if (data.sendEmailInvitation) {
|
||||||
|
showToast(
|
||||||
|
t("email_invite_team", {
|
||||||
|
email: data.usernameOrEmail,
|
||||||
|
}),
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
showToast(error.message, "error");
|
showToast(error.message, "error");
|
||||||
@@ -270,17 +278,15 @@ export default function TeamListItem(props: Props) {
|
|||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<DropdownItem
|
||||||
type="button"
|
|
||||||
color="destructive"
|
color="destructive"
|
||||||
size="lg"
|
type="button"
|
||||||
StartIcon={FiLogOut}
|
StartIcon={FiLogOut}
|
||||||
className="w-full rounded-none"
|
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}>
|
}}>
|
||||||
{t("leave_team")}
|
{t("leave_team")}
|
||||||
</Button>
|
</DropdownItem>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
variety="danger"
|
variety="danger"
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export function TeamsListing() {
|
|||||||
emptyTitle="no_teams"
|
emptyTitle="no_teams"
|
||||||
emptyDescription="no_teams_description"
|
emptyDescription="no_teams_description"
|
||||||
features={features}
|
features={features}
|
||||||
background="/banners/teams.jpg"
|
background="/tips/teams.jpg"
|
||||||
buttons={
|
buttons={
|
||||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
|||||||
Loaded 100 of 153 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user