Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8176b2eb4 | ||
|
|
6c51e2a970 | ||
|
|
3a08eb3dca | ||
|
|
cee905bc5a | ||
|
|
72fd6334c6 | ||
|
|
63f51abd84 | ||
|
|
6e856b3bd1 | ||
|
|
5fceee27fe | ||
|
|
4f590ebbf7 | ||
|
|
736cd63509 | ||
|
|
3aaa1cde0d | ||
|
|
02d32321aa | ||
|
|
697f6db2c4 | ||
|
|
83364a9e97 | ||
|
|
1620ae660f | ||
|
|
605f9c61f3 | ||
|
|
564862369f | ||
|
|
be6cf4705c | ||
|
|
a2457ab619 | ||
|
|
867966dd75 | ||
|
|
08ad4c07ea | ||
|
|
78da57055e | ||
|
|
d5c3d9d967 | ||
|
|
aa0e9e6a02 | ||
|
|
cad97f1779 | ||
|
|
31ee689672 | ||
|
|
3edb6f5ffd | ||
|
|
a8825badec | ||
|
|
04c634ec4b | ||
|
|
8dd9b8d74b | ||
|
|
0365157638 | ||
|
|
a571d40958 | ||
|
|
69491d5700 | ||
|
|
20022e2cdd | ||
|
|
492858b45b | ||
|
|
b49ad8294b | ||
|
|
aa30ff7d1a | ||
|
|
cfa720a523 | ||
|
|
9ab196c30e | ||
|
|
47e13cd2fd | ||
|
|
74ab6d209e | ||
|
|
46f0b55a45 | ||
|
|
e4893c2218 | ||
|
|
fa17139e8f | ||
|
|
1cdc97dd37 | ||
|
|
f630e8af40 | ||
|
|
fe0745121b | ||
|
|
f6d7568d0a | ||
|
|
520e7fe036 | ||
|
|
1bf50e09cd | ||
|
|
d1ca7b4bdd | ||
|
|
4e2473531f |
@@ -12,13 +12,13 @@ Fixes # (issue)
|
||||
|
||||
## Type of change
|
||||
|
||||
<!-- Please delete options that are not relevant. -->
|
||||
<!-- Please delete bullets that are not relevant. -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] Chore (refactoring code, technical debt, workflow improvements)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
- Bug fix (non-breaking change which fixes an issue)
|
||||
- Chore (refactoring code, technical debt, workflow improvements)
|
||||
- New feature (non-breaking change which adds functionality)
|
||||
- Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- This change requires a documentation update
|
||||
|
||||
## How should this be tested?
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"problemMatcher": [
|
||||
{
|
||||
"owner": "tsc-absolute",
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": "(?:^|\\s)([^\\s].*)[\\(:](\\d+)[,:](\\d+)(?:\\):\\s+|\\s+-\\s+)(error|warning|info)\\s+(TS\\d+)\\s*:\\s*(.*)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"column": 3,
|
||||
"severity": 4,
|
||||
"code": 5,
|
||||
"message": 6
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,8 +10,10 @@ jobs:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: ./.github/actions/dangerous-git-checkout
|
||||
- uses: ./.github/actions/yarn-install
|
||||
|
||||
- name: Show info
|
||||
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||
|
||||
- run: yarn type-check
|
||||
- name: Configure TSC problem matcher
|
||||
run: |
|
||||
echo "::remove-matcher owner=tsc::"
|
||||
echo "::add-matcher::.github/matchers/tsc-absolute.json"
|
||||
- run: yarn type-check:ci
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
name: Cron - downgradeUsers
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
# "Scheduled workflows run on the latest commit on the default or base branch."
|
||||
# — https://docs.github.com/en/actions/learn-github-actions/events-that-trigger-workflows#schedule
|
||||
schedule:
|
||||
# Runs “At minute 0, 15, 30, and 45.” (see https://crontab.guru)
|
||||
- cron: "0,15,30,45 * * * *"
|
||||
# Runs “Every month at 1st (see https://crontab.guru)
|
||||
- cron: "0 0 1 * *"
|
||||
jobs:
|
||||
cron-downgradeUsers:
|
||||
env:
|
||||
|
||||
@@ -350,8 +350,7 @@ We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:is
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=dark">
|
||||
<img alt="Bounties of cal" src="https://console.algora.io/api/og/cal/bounties.png?p=0&status=open&theme=light">
|
||||
</picture>
|
||||
</a>
|
||||
|
||||
</a>
|
||||
|
||||
<!-- CONTRIBUTORS -->
|
||||
|
||||
@@ -456,12 +455,12 @@ following
|
||||
|
||||
1. Open [Zoho API Console](https://api-console.zoho.com/) and sign into your account, or create a new one.
|
||||
2. From within the API console page, go to "Applications".
|
||||
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
||||
3. Click "ADD CLIENT" button top right and select "Server-based Applications".
|
||||
4. Fill in any information you want in the "Client Details" tab
|
||||
5. Go to tab "Client Secret" tab.
|
||||
5. Go to tab "Client Secret" tab.
|
||||
6. Now copy the Client ID and Client Secret to your .env file into the `ZOHOCRM_CLIENT_ID` and `ZOHOCRM_CLIENT_SECRET` fields.
|
||||
7. Set the Redirect URL for OAuth `<Cal.com URL>/api/integrations/zohocrm/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
||||
8. In the "Settings" section check the "Multi-DC" option if you wish to use the same OAuth credentials for all data centers.
|
||||
9. Click the "Save"/ "UPDATE" button at the bottom footer.
|
||||
10. You're good to go. Now you can easily add your ZohoCRM integration in the Cal.com settings.
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"lint:report": "eslint . --format json --output-file ../../lint-results/docs.json",
|
||||
"start": "PORT=4000 next start",
|
||||
"type-check": "tsc --pretty --noEmit"
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit"
|
||||
},
|
||||
"author": "Cal.com, Inc.",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -167,7 +167,7 @@ const Codes: Record<string, Record<string, (...args: any[]) => string>> = {
|
||||
const height = getDimension(previewState.inline.height);
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
@@ -187,12 +187,12 @@ function MyComponent() {
|
||||
}) => {
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
const cal = await getCalApi();
|
||||
Cal("floatingButton", ${floatingButtonArg});
|
||||
cal("floatingButton", ${floatingButtonArg});
|
||||
${uiInstructionCode}
|
||||
})();
|
||||
}, [])
|
||||
@@ -201,7 +201,7 @@ function MyComponent() {
|
||||
"element-click": ({ calLink, uiInstructionCode }: { calLink: string; uiInstructionCode: string }) => {
|
||||
return code`
|
||||
import Cal, { getCalApi } from "@calcom/embed-react";
|
||||
|
||||
import { useEffect } from "react";
|
||||
function MyComponent() {
|
||||
useEffect(()=>{
|
||||
(async function () {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AdminRequired } from "components/ui/AdminRequired";
|
||||
import noop from "lodash/noop";
|
||||
import { noop } from "lodash";
|
||||
import type { LinkProps } from "next/link";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
@@ -8,54 +8,61 @@ import { FiLink } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { Props } from "./pages/AvailabilityPage";
|
||||
|
||||
const excludeNullValues = (value: unknown) => !!value;
|
||||
|
||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
return locations.length ? (
|
||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
||||
{locations.map((location, index) => {
|
||||
const eventLocationType = getEventLocationType(location.type);
|
||||
if (!eventLocationType) {
|
||||
// It's possible that the location app got uninstalled
|
||||
return null;
|
||||
}
|
||||
const renderLocations = locations.map((location, index) => {
|
||||
const eventLocationType = getEventLocationType(location.type);
|
||||
if (!eventLocationType) {
|
||||
// It's possible that the location app got uninstalled
|
||||
return null;
|
||||
}
|
||||
if (eventLocationType.variable === "hostDefault") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const translateAbleKeys = [
|
||||
"attendee_in_person",
|
||||
"in_person",
|
||||
"attendee_phone_number",
|
||||
"link_meeting",
|
||||
"organizer_phone_number",
|
||||
];
|
||||
const translateAbleKeys = [
|
||||
"attendee_in_person",
|
||||
"in_person",
|
||||
"attendee_phone_number",
|
||||
"link_meeting",
|
||||
"organizer_phone_number",
|
||||
];
|
||||
|
||||
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
||||
const locationKey = z.string().default("").parse(locationKeyToString(location));
|
||||
const translatedLocation = location.type.startsWith("integrations:")
|
||||
? eventLocationType.label
|
||||
: translateAbleKeys.includes(locationKey)
|
||||
? t(locationKey)
|
||||
: locationKey;
|
||||
|
||||
const translatedLocation = location.type.startsWith("integrations:")
|
||||
? eventLocationType.label
|
||||
: translateAbleKeys.includes(locationKey)
|
||||
? t(locationKey)
|
||||
: locationKey;
|
||||
|
||||
return (
|
||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||
) : (
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
className={classNames(
|
||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
||||
)}
|
||||
alt={`${eventLocationType.label} icon`}
|
||||
/>
|
||||
return (
|
||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||
<FiLink className="dark:text-darkgray-600 min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||
) : (
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
className={classNames(
|
||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
||||
)}
|
||||
<Tooltip content={translatedLocation}>
|
||||
<p className="line-clamp-1">{translatedLocation}</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
alt={`${eventLocationType.label} icon`}
|
||||
/>
|
||||
)}
|
||||
<Tooltip content={translatedLocation}>
|
||||
<p className="line-clamp-1">{translatedLocation}</p>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
const filteredLocations = renderLocations.filter(excludeNullValues) as JSX.Element[];
|
||||
return filteredLocations.length ? (
|
||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
||||
{filteredLocations}
|
||||
</div>
|
||||
) : null;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ import { useState } from "react";
|
||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||
import { getEventLocationType } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||
import "@calcom/dayjs/locales";
|
||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { formatTime } from "@calcom/lib/date-fns";
|
||||
|
||||
@@ -77,7 +77,7 @@ export default function CancelBooking(props: Props) {
|
||||
{t("nevermind")}
|
||||
</Button>
|
||||
<Button
|
||||
className="flex justify-center"
|
||||
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast flex justify-center"
|
||||
data-testid="cancel"
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -133,10 +133,7 @@ export const SlotPicker = ({
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
usernameList: users,
|
||||
startTime:
|
||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
||||
: browsingDate?.startOf("month"),
|
||||
startTime: dayjs().startOf("day"),
|
||||
endTime: browsingDate?.endOf("month"),
|
||||
timeZone,
|
||||
duration,
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import type { FC } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import type { ITimezoneOption } from "@calcom/ui";
|
||||
import { TimezoneSelect } from "@calcom/ui";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import { timeZone } from "../../lib/clock";
|
||||
|
||||
type Props = {
|
||||
@@ -15,8 +12,6 @@ type Props = {
|
||||
|
||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||
const query = useMeQuery();
|
||||
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTimeZone(timeZone());
|
||||
@@ -28,59 +23,16 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||
}
|
||||
}, [selectedTimeZone, onSelectTimeZone]);
|
||||
|
||||
const customStyles = {
|
||||
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
||||
padding: "10px 12px !important",
|
||||
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
||||
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
||||
...(userTheme === "dark" && {
|
||||
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
||||
}),
|
||||
}),
|
||||
control: () => ({
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "transparent !important",
|
||||
minWidth: "5rem",
|
||||
height: "24px",
|
||||
minHeight: "24px !important",
|
||||
boxShadow: "none !important",
|
||||
}),
|
||||
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
||||
...provided,
|
||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
||||
...(userTheme === "dark" && {
|
||||
color: "#a5a5a5 !important",
|
||||
}),
|
||||
}),
|
||||
menu: (provided: object) => ({
|
||||
...provided,
|
||||
minWidth: "20rem",
|
||||
margin: "8px 0 0 -20px",
|
||||
}),
|
||||
valueContainer: (provided: object) => ({
|
||||
...provided,
|
||||
padding: "0 0 0 6px",
|
||||
}),
|
||||
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
||||
...provided,
|
||||
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
|
||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
||||
marginLeft: 4,
|
||||
padding: 0,
|
||||
...(userTheme === "dark" && {
|
||||
color: "#80868B",
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
return !!selectedTimeZone ? (
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
classNames={{
|
||||
singleValue: () => "dark:text-darkgray-600 text-gray-600",
|
||||
menu: () => "!w-64 max-w-[90vw]",
|
||||
}}
|
||||
variant="minimal"
|
||||
value={selectedTimeZone}
|
||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||
className="flex h-6 text-sm font-medium"
|
||||
styles={customStyles}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
@@ -17,8 +17,8 @@ export function TimezoneDropdown({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit max-w-[20rem] items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
|
||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -205,6 +205,7 @@ const routerQuerySchema = z
|
||||
const BookingPage = ({
|
||||
eventType,
|
||||
booking,
|
||||
currentSlotBooking,
|
||||
profile,
|
||||
isDynamicGroupBooking,
|
||||
recurringEventCount,
|
||||
@@ -287,6 +288,7 @@ const BookingPage = ({
|
||||
return router.push({
|
||||
pathname: `/booking/${uid}`,
|
||||
query: {
|
||||
isSuccessBookingPage: true,
|
||||
allRemainingBookings: true,
|
||||
email: bookingForm.getValues("responses.email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
@@ -583,23 +585,27 @@ const BookingPage = ({
|
||||
<div className="text-bookinghighlight flex items-start text-sm">
|
||||
<FiUser
|
||||
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
currentSlotBooking &&
|
||||
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
: currentSlotBooking &&
|
||||
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-bookinghighlight"
|
||||
}`}
|
||||
/>
|
||||
<p
|
||||
className={`${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
currentSlotBooking &&
|
||||
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
: currentSlotBooking &&
|
||||
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-bookinghighlight"
|
||||
} mb-2 font-medium`}>
|
||||
{booking
|
||||
? eventType.seatsPerTimeSlot - booking.attendees.length
|
||||
{currentSlotBooking
|
||||
? eventType.seatsPerTimeSlot - currentSlotBooking.attendees.length
|
||||
: eventType.seatsPerTimeSlot}{" "}
|
||||
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
@@ -631,6 +637,7 @@ const BookingPage = ({
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
className="dark:bg-darkmodebrand dark:hover:border-darkmodebrand dark:text-darkmodebrandcontrast bg-brand hover:border-brand text-brandcontrast"
|
||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
||||
loading={mutation.isLoading || recurringMutation.isLoading}>
|
||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||
|
||||
@@ -33,6 +33,7 @@ interface ISetLocationDialog {
|
||||
saveLocation: (newLocationType: EventLocationType["type"], details: { [key: string]: string }) => void;
|
||||
selection?: LocationOption;
|
||||
booking?: BookingItem;
|
||||
isTeamEvent?: boolean;
|
||||
defaultValues?: LocationObject[];
|
||||
setShowLocationModal: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
isOpenDialog: boolean;
|
||||
@@ -74,6 +75,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
saveLocation,
|
||||
selection,
|
||||
booking,
|
||||
isTeamEvent,
|
||||
setShowLocationModal,
|
||||
isOpenDialog,
|
||||
defaultValues,
|
||||
@@ -290,7 +292,9 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
query={locationsQuery}
|
||||
success={({ data }) => {
|
||||
if (!data.length) return null;
|
||||
const locationOptions = [...data];
|
||||
const locationOptions = [...data].filter((option) => {
|
||||
return !isTeamEvent ? option.label !== "Conferencing" : true;
|
||||
});
|
||||
if (booking) {
|
||||
locationOptions.map((location) =>
|
||||
location.options.filter((l) => !["phone", "attendeeInPerson"].includes(l.value))
|
||||
|
||||
@@ -4,13 +4,15 @@ import type { OptionProps, SingleValueProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { NewScheduleButton } from "@calcom/features/schedules";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
||||
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
@@ -52,23 +54,24 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||
|
||||
const AvailabilitySelect = ({
|
||||
className = "",
|
||||
isLoading,
|
||||
schedules,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
name: string;
|
||||
value: number;
|
||||
isLoading: boolean;
|
||||
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
|
||||
onBlur: () => void;
|
||||
onChange: (value: AvailabilityOption | null) => void;
|
||||
}) => {
|
||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||
const { t } = useLocale();
|
||||
|
||||
if (isLoading) {
|
||||
return <SelectSkeletonLoader />;
|
||||
}
|
||||
|
||||
const schedules = data?.schedules || [];
|
||||
|
||||
const options = schedules.map((schedule) => ({
|
||||
value: schedule.id,
|
||||
label: schedule.name,
|
||||
@@ -107,6 +110,7 @@ const EventTypeScheduleDetails = () => {
|
||||
const { watch } = useFormContext<FormValues>();
|
||||
|
||||
const scheduleId = watch("schedule");
|
||||
|
||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
||||
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
||||
@@ -174,6 +178,19 @@ const EventTypeScheduleDetails = () => {
|
||||
|
||||
const EventTypeSchedule = () => {
|
||||
const { t } = useLocale();
|
||||
const { data: schedules, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||
|
||||
if (!schedules?.schedules.length && !isLoading)
|
||||
return (
|
||||
<EmptyScreen
|
||||
Icon={FiClock}
|
||||
headline={t("new_schedule_heading")}
|
||||
description={t("new_schedule_description")}
|
||||
buttonRaw={<NewScheduleButton fromEventType />}
|
||||
border={false}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@@ -183,14 +200,18 @@ const EventTypeSchedule = () => {
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => (
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
schedules={schedules?.schedules || []}
|
||||
isLoading={isLoading}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -88,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
label={t("minimum_booking_notice")}
|
||||
type="number"
|
||||
placeholder="0"
|
||||
min={0}
|
||||
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
|
||||
/>
|
||||
<input type="hidden" ref={ref} {...passThroughProps} />
|
||||
|
||||
@@ -35,16 +35,6 @@ const getLocationFromType = (
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
||||
for (const locationType of options) {
|
||||
for (const location of locationType.options) {
|
||||
if (location.value === type && location.disabled === false) {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const EventSetupTab = (
|
||||
props: Pick<
|
||||
EventTypeSetupProps,
|
||||
@@ -53,12 +43,16 @@ export const EventSetupTab = (
|
||||
) => {
|
||||
const { t } = useLocale();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { eventType, locationOptions, team, destinationCalendar } = props;
|
||||
const { eventType, team, destinationCalendar } = props;
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
const [editingLocationType, setEditingLocationType] = useState<string>("");
|
||||
const [selectedLocation, setSelectedLocation] = useState<LocationOption | undefined>(undefined);
|
||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata.multipleDuration);
|
||||
|
||||
const locationOptions = props.locationOptions.filter((option) => {
|
||||
return !team ? option.label !== "Conferencing" : true;
|
||||
});
|
||||
|
||||
const multipleDurationOptions = [5, 10, 15, 20, 25, 30, 45, 50, 60, 75, 80, 90, 120, 180].map((mins) => ({
|
||||
value: mins,
|
||||
label: t("multiple_duration_mins", { count: mins }),
|
||||
@@ -409,6 +403,7 @@ export const EventSetupTab = (
|
||||
|
||||
{/* We portal this modal so we can submit the form inside. Otherwise we get issues submitting two forms at once */}
|
||||
<EditLocationDialog
|
||||
isTeamEvent={!!team}
|
||||
isOpenDialog={showLocationModal}
|
||||
setShowLocationModal={setShowLocationModal}
|
||||
saveLocation={saveLocation}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export type EventTypeDescriptionSafeProps = {
|
||||
eventType: { description: string | null };
|
||||
eventType: { description: string | null; descriptionAsSafeHTML: string | null };
|
||||
};
|
||||
|
||||
export const EventTypeDescriptionSafeHTML = ({ eventType }: EventTypeDescriptionSafeProps) => {
|
||||
const props: JSX.IntrinsicElements["div"] = { suppressHydrationWarning: true };
|
||||
// @ts-expect-error: @see packages/prisma/middleware/eventTypeDescriptionParseAndSanitize.ts
|
||||
if (eventType.description) props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML };
|
||||
if (eventType.description)
|
||||
props.dangerouslySetInnerHTML = { __html: eventType.descriptionAsSafeHTML || "" };
|
||||
return <div {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -247,7 +247,7 @@ function EventTypeSingleLayout({
|
||||
tooltip={t("embed")}
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
color="destructive"
|
||||
variant="icon"
|
||||
StartIcon={FiTrash}
|
||||
tooltip={t("delete")}
|
||||
|
||||
@@ -75,7 +75,9 @@ export function CalendarSwitch(props: {
|
||||
/>
|
||||
{props.defaultSelected && (
|
||||
<Badge variant="gray">
|
||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
<div className="flex">
|
||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
</div>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,14 @@ import Link from "next/link";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import type { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
type TeamType = NonNullable<TeamWithMembers>;
|
||||
type MembersType = TeamType["members"];
|
||||
type MemberType = MembersType[number];
|
||||
type MemberType = MembersType[number] & { safeBio: string | null };
|
||||
|
||||
type TeamTypeWithSafeHtml = Omit<TeamType, "members"> & { members: MemberType[] };
|
||||
|
||||
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
||||
const { t } = useLocale();
|
||||
@@ -30,7 +31,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
<>
|
||||
<div
|
||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: member.safeBio || "" }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
@@ -43,7 +44,7 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
);
|
||||
};
|
||||
|
||||
const Members = ({ members, teamName }: { members: MembersType; teamName: string | null }) => {
|
||||
const Members = ({ members, teamName }: { members: MemberType[]; teamName: string | null }) => {
|
||||
if (!members || members.length === 0) {
|
||||
return null;
|
||||
}
|
||||
@@ -57,7 +58,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
|
||||
);
|
||||
};
|
||||
|
||||
const Team = ({ team }: { team: TeamType }) => {
|
||||
const Team = ({ team }: { team: TeamTypeWithSafeHtml }) => {
|
||||
return (
|
||||
<div>
|
||||
<Members members={team.members} teamName={team.name} />
|
||||
|
||||
@@ -10,6 +10,8 @@ import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||
import { FeatureProvider } from "@calcom/features/flags/context/provider";
|
||||
import { useFlags } from "@calcom/features/flags/hooks";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { MetaProvider } from "@calcom/ui";
|
||||
|
||||
@@ -58,6 +60,11 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
||||
return <I18nextAdapter {...passedProps} />;
|
||||
};
|
||||
|
||||
function FeatureFlagsProvider({ children }: { children: React.ReactNode }) {
|
||||
const flags = useFlags();
|
||||
return <FeatureProvider value={flags}>{children}</FeatureProvider>;
|
||||
}
|
||||
|
||||
const AppProviders = (props: AppPropsWithChildren) => {
|
||||
const session = trpc.viewer.public.session.useQuery().data;
|
||||
// No need to have intercom on public pages - Good for Page Performance
|
||||
@@ -85,7 +92,9 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
storageKey={storageKey}
|
||||
forcedTheme={forcedTheme}
|
||||
attribute="class">
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
<FeatureFlagsProvider>
|
||||
<MetaProvider>{props.children}</MetaProvider>
|
||||
</FeatureFlagsProvider>
|
||||
</ThemeProvider>
|
||||
</TooltipProvider>
|
||||
</CustomI18nextProvider>
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import type { Prisma, PrismaClient } from "@prisma/client";
|
||||
import type { z } from "zod";
|
||||
|
||||
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import { bookingResponsesDbSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import type { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
|
||||
type BookingSelect = {
|
||||
description: true;
|
||||
@@ -45,11 +44,7 @@ function getResponsesFromOldBooking(
|
||||
};
|
||||
}
|
||||
|
||||
async function getBooking(
|
||||
prisma: PrismaClient,
|
||||
uid: string,
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">
|
||||
) {
|
||||
async function getBooking(prisma: PrismaClient, uid: string) {
|
||||
const rawBooking = await prisma.booking.findFirst({
|
||||
where: {
|
||||
uid,
|
||||
@@ -82,9 +77,7 @@ async function getBooking(
|
||||
return rawBooking;
|
||||
}
|
||||
|
||||
const booking = getBookingWithResponses(rawBooking, {
|
||||
bookingFields,
|
||||
});
|
||||
const booking = getBookingWithResponses(rawBooking);
|
||||
|
||||
if (booking) {
|
||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||
@@ -104,20 +97,11 @@ export const getBookingWithResponses = <
|
||||
};
|
||||
}>
|
||||
>(
|
||||
booking: T,
|
||||
eventType: {
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields> & z.BRAND<"HAS_SYSTEM_FIELDS">;
|
||||
}
|
||||
booking: T
|
||||
) => {
|
||||
return {
|
||||
...booking,
|
||||
responses: getBookingResponsesPartialSchema({
|
||||
eventType: {
|
||||
bookingFields: eventType.bookingFields,
|
||||
},
|
||||
// An existing booking can have data from any number of views, so the schema should consider ALL_VIEWS
|
||||
view: "ALL_VIEWS",
|
||||
}).parse(booking.responses || getResponsesFromOldBooking(booking)),
|
||||
};
|
||||
responses: bookingResponsesDbSchema.parse(booking.responses || getResponsesFromOldBooking(booking)),
|
||||
} as Omit<T, "responses"> & { responses: z.infer<typeof bookingResponsesDbSchema> };
|
||||
};
|
||||
export default getBooking;
|
||||
|
||||
@@ -99,6 +99,14 @@ const nextConfig = {
|
||||
skipDefaultConversion: true,
|
||||
preventFullImport: true,
|
||||
},
|
||||
"@calcom/features/insights/components": {
|
||||
transform: "@calcom/features/insights/components/{{member}}",
|
||||
skipDefaultConversion: true,
|
||||
preventFullImport: true,
|
||||
},
|
||||
lodash: {
|
||||
transform: "lodash/{{member}}",
|
||||
},
|
||||
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
||||
// "@calcom/ui": {
|
||||
// transform: "@calcom/ui/components/{{member}}",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.7.3",
|
||||
"version": "2.7.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -11,6 +11,7 @@
|
||||
"dx": "yarn dev",
|
||||
"test-codegen": "yarn playwright codegen http://localhost:3000",
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint . --ignore-path .gitignore",
|
||||
@@ -60,6 +61,7 @@
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tanstack/react-query": "^4.3.9",
|
||||
"@tremor/react": "^2.0.0",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@vercel/edge-config": "^0.1.1",
|
||||
"@vercel/edge-functions-ui": "^0.2.1",
|
||||
@@ -118,6 +120,7 @@
|
||||
"react-window": "^1.8.7",
|
||||
"remark": "^14.0.2",
|
||||
"rrule": "^2.7.1",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"schema-dts": "^1.1.0",
|
||||
"short-uuid": "^4.2.0",
|
||||
"strip-markdown": "^5.0.0",
|
||||
@@ -155,6 +158,7 @@
|
||||
"@types/react-phone-number-input": "^3.0.14",
|
||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||
"@types/react-window": "^1.8.5",
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@types/stripe": "^8.0.417",
|
||||
"@types/uuid": "8.3.1",
|
||||
"autoprefixer": "^10.4.12",
|
||||
|
||||
+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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { FiFileText, FiCheck, FiBookOpen, FiChevronRight } from "@calcom/ui/components/icon";
|
||||
import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
|
||||
|
||||
import { ssgInit } from "@server/lib/ssg";
|
||||
|
||||
@@ -40,6 +40,43 @@ export default function Custom404() {
|
||||
const isSubpage = router.asPath.includes("/", 2) || isSuccessPage;
|
||||
const isSignup = router.asPath.startsWith("/signup");
|
||||
const isCalcom = process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com";
|
||||
/**
|
||||
* If we're on 404 and the route is insights it means it is disabled
|
||||
* TODO: Abstract this for all disabled features
|
||||
**/
|
||||
const isInsights = router.asPath.startsWith("/insights");
|
||||
if (isInsights) {
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
title="Feature is currently disabled"
|
||||
description={t("404_page_not_found")}
|
||||
nextSeoProps={{
|
||||
nofollow: true,
|
||||
noindex: true,
|
||||
}}
|
||||
/>
|
||||
<div className="min-h-screen bg-white px-4" data-testid="404-page">
|
||||
<main className="mx-auto max-w-xl pt-16 pb-6 sm:pt-24">
|
||||
<div className="text-center">
|
||||
<p className="text-sm font-semibold uppercase tracking-wide text-black">{t("error_404")}</p>
|
||||
<h1 className="font-cal mt-2 text-4xl font-extrabold text-gray-900 sm:text-5xl">
|
||||
Feature is currently disabled
|
||||
</h1>
|
||||
</div>
|
||||
<div className="mt-12">
|
||||
<div className="mt-8">
|
||||
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
|
||||
{t("or_go_back_home")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -24,7 +24,7 @@ import defaultEvents, {
|
||||
} from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||
@@ -147,7 +147,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<>
|
||||
<div
|
||||
className=" dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: props.safeBio }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -343,6 +343,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
const eventTypes = eventTypesRaw.map((eventType) => ({
|
||||
...eventType,
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
}));
|
||||
|
||||
const isSingleUser = users.length === 1;
|
||||
@@ -352,9 +353,12 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
})
|
||||
: [];
|
||||
|
||||
const safeBio = markdownToSafeHTML(user.bio) || "";
|
||||
|
||||
return {
|
||||
props: {
|
||||
users,
|
||||
safeBio,
|
||||
profile,
|
||||
user: {
|
||||
emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { addListFormatting } from "@calcom/lib/markdownIt";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import type { User } from "@calcom/prisma/client";
|
||||
|
||||
import { isBrandingHidden } from "@lib/isBrandingHidden";
|
||||
@@ -59,7 +59,6 @@ Type.isThemeSupported = true;
|
||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
// load server side dependencies
|
||||
const MarkdownIt = await import("markdown-it").then((mod) => mod.default);
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const { privacyFilteredLocations } = await import("@calcom/app-store/locations");
|
||||
const { parseRecurringEvent } = await import("@calcom/lib/isRecurringEvent");
|
||||
@@ -124,9 +123,6 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
if (!user || !user.eventTypes.length) return { notFound: true };
|
||||
|
||||
const [eventType]: ((typeof user.eventTypes)[number] & {
|
||||
@@ -153,7 +149,7 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
||||
locations: privacyFilteredLocations(locations),
|
||||
descriptionAsSafeHTML: eventType.description ? addListFormatting(md.render(eventType.description)) : null,
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
});
|
||||
// Check if the user you are logging into has any active teams or premium user name
|
||||
const hasActiveTeam =
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
getUsernameList,
|
||||
} from "@calcom/lib/defaultEvents";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import prisma, { bookEventTypeSelect } from "@calcom/prisma";
|
||||
import {
|
||||
customInputSchema,
|
||||
@@ -189,6 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
slug: u.username,
|
||||
theme: u.theme,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
};
|
||||
})[0];
|
||||
|
||||
@@ -217,7 +219,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
rescheduleUid = bookingSeat.booking.uid;
|
||||
attendeeEmail = bookingSeat.attendee.email;
|
||||
}
|
||||
} else if (eventTypeRaw.seatsPerTimeSlot && query.duration && query.date) {
|
||||
}
|
||||
|
||||
if (query.duration) {
|
||||
// If it's not reschedule but event Type has seats we should obtain
|
||||
// the bookingUid regardless and use it to get the booking
|
||||
const currentSeats = await prisma.booking.findFirst({
|
||||
@@ -237,11 +241,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid || query.bookingUid || bookingUidWithSeats) {
|
||||
booking = await getBooking(
|
||||
prisma,
|
||||
rescheduleUid || query.bookingUid || bookingUidWithSeats || "",
|
||||
eventTypeObject.bookingFields
|
||||
);
|
||||
booking = await getBooking(prisma, rescheduleUid || query.bookingUid || bookingUidWithSeats || "");
|
||||
}
|
||||
|
||||
if (rescheduleEventTypeHasSeats && booking?.attendees && booking?.attendees.length > 0) {
|
||||
@@ -285,12 +285,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
: eventType.recurringEvent.count)) ||
|
||||
null;
|
||||
|
||||
const currentSlotBooking = await getBooking(prisma, bookingUidWithSeats || "");
|
||||
|
||||
return {
|
||||
props: {
|
||||
away: user.away,
|
||||
profile,
|
||||
eventType: eventTypeObject,
|
||||
booking,
|
||||
currentSlotBooking: currentSlotBooking,
|
||||
recurringEventCount,
|
||||
trpcState: ssr.dehydrate(),
|
||||
isDynamicGroupBooking,
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { updateQuantitySubscriptionFromStripe } from "@calcom/features/ee/teams/lib/payments";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
const querySchema = z.object({
|
||||
page: z.coerce.number().min(0).optional().default(0),
|
||||
});
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||
@@ -11,10 +19,35 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* Remove this endpoint
|
||||
*/
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
const pageSize = 90; // Adjust this value based on the total number of teams and the available processing time
|
||||
let { page: pageNumber } = querySchema.parse(req.query);
|
||||
|
||||
while (true) {
|
||||
const teams = await prisma.team.findMany({
|
||||
where: {
|
||||
slug: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
skip: pageNumber * pageSize,
|
||||
take: pageSize,
|
||||
});
|
||||
|
||||
if (teams.length === 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (const team of teams) {
|
||||
await updateQuantitySubscriptionFromStripe(team.id);
|
||||
await delay(100); // Adjust the delay as needed to avoid rate limiting
|
||||
}
|
||||
|
||||
pageNumber++;
|
||||
}
|
||||
|
||||
res.json({ ok: true });
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ResetPasswordRequest } from "@prisma/client";
|
||||
import debounce from "lodash/debounce";
|
||||
import { debounce } from "lodash";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import debounce from "lodash/debounce";
|
||||
import { debounce } from "lodash";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
@@ -94,6 +94,7 @@ export default function Availability() {
|
||||
data: { schedule: scheduleId },
|
||||
} = useTypedQuery(querySchema);
|
||||
|
||||
const { fromEventType } = router.query;
|
||||
const { timeFormat } = me.data || { timeFormat: null };
|
||||
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId },
|
||||
@@ -153,7 +154,7 @@ export default function Availability() {
|
||||
|
||||
return (
|
||||
<Shell
|
||||
backPath="/availability"
|
||||
backPath={fromEventType ? true : "/availability"}
|
||||
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")}
|
||||
heading={
|
||||
<Controller
|
||||
@@ -200,7 +201,14 @@ export default function Availability() {
|
||||
<VerticalDivider />
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button StartIcon={FiTrash} variant="icon" color="destructive" aria-label={t("delete")} />
|
||||
<Button
|
||||
StartIcon={FiTrash}
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
aria-label={t("delete")}
|
||||
disabled={schedule?.isLastSchedule}
|
||||
tooltip={t("requires_at_least_one_schedule")}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={deleteMutation.isLoading}
|
||||
|
||||
@@ -93,7 +93,13 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
key={schedule.id}
|
||||
schedule={schedule}
|
||||
updateDefault={updateMutation.mutate}
|
||||
deleteFunction={deleteMutation.mutate}
|
||||
deleteFunction={() => {
|
||||
if (schedules.length === 1) {
|
||||
showToast(t("requires_at_least_one_schedule"), "error");
|
||||
} else {
|
||||
deleteMutation.mutate;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -180,7 +180,7 @@ const querySchema = z.object({
|
||||
cancel: stringToBoolean,
|
||||
changes: stringToBoolean,
|
||||
reschedule: stringToBoolean,
|
||||
isSuccessBookingPage: z.string().optional(),
|
||||
isSuccessBookingPage: stringToBoolean,
|
||||
formerTime: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
seatReferenceUid: z.string().optional(),
|
||||
@@ -266,7 +266,11 @@ export default function Success(props: SuccessProps) {
|
||||
const giphyImage = giphyAppData?.thankYouPage;
|
||||
|
||||
const eventName = getEventName(eventNameObject, true);
|
||||
const needsConfirmation = eventType.requiresConfirmation && reschedule != true;
|
||||
// Confirmation can be needed in two cases as of now
|
||||
// - Event Type has require confirmation option enabled always
|
||||
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
|
||||
// - It's a paid event and payment is pending.
|
||||
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING;
|
||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||
|
||||
const isCancelled =
|
||||
@@ -485,7 +489,7 @@ export default function Success(props: SuccessProps) {
|
||||
</>
|
||||
)}
|
||||
<div className="font-medium">{t("what")}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">{eventName}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">{props.bookingInfo.title}</div>
|
||||
<div className="font-medium">{t("when")}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">
|
||||
{reschedule && !!formerTime && (
|
||||
@@ -527,7 +531,7 @@ export default function Success(props: SuccessProps) {
|
||||
</div>
|
||||
)}
|
||||
{bookingInfo?.attendees.map((attendee) => (
|
||||
<div key={attendee.name} className="mb-3 last:mb-0">
|
||||
<div key={attendee.name + attendee.email} className="mb-3 last:mb-0">
|
||||
{attendee.name && <p>{attendee.name}</p>}
|
||||
<p data-testid={`attendee-${attendee.email}`} className="text-bookinglight">
|
||||
{attendee.email}
|
||||
@@ -1087,8 +1091,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
};
|
||||
}
|
||||
|
||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw, eventTypeRaw);
|
||||
|
||||
const bookingInfo = getBookingWithResponses(bookingInfoRaw);
|
||||
// @NOTE: had to do this because Server side cant return [Object objects]
|
||||
// probably fixable with json.stringify -> json.parse
|
||||
bookingInfo["startTime"] = (bookingInfo?.startTime as Date)?.toISOString() as unknown as Date;
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { LocationObject } from "@calcom/core/location";
|
||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getWorkingHours } from "@calcom/lib/availability";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -119,6 +120,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
hideBranding: u.hideBranding,
|
||||
timeZone: u.timeZone,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(hashedLink.eventType.description),
|
||||
});
|
||||
|
||||
const [user] = users;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
||||
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
@@ -93,6 +94,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
brandColor: u.brandColor,
|
||||
darkBrandColor: u.darkBrandColor,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
};
|
||||
})[0];
|
||||
|
||||
@@ -120,6 +122,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
profile,
|
||||
eventType: eventTypeObject,
|
||||
booking: null,
|
||||
currentSlotBooking: null,
|
||||
trpcState: ssr.dehydrate(),
|
||||
recurringEventCount,
|
||||
isDynamicGroupBooking: false,
|
||||
|
||||
@@ -65,6 +65,7 @@ export type FormValues = {
|
||||
hostPhoneNumber?: string;
|
||||
displayLocationPublicly?: boolean;
|
||||
phone?: string;
|
||||
hostDefault?: string;
|
||||
}[];
|
||||
customInputs: CustomInputParsed[];
|
||||
schedule: number | null;
|
||||
|
||||
@@ -141,7 +141,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
||||
</div>
|
||||
<EventTypeDescription
|
||||
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||
eventType={type}
|
||||
eventType={{ ...type, descriptionAsSafeHTML: type.safeDescription }}
|
||||
shortenDescription
|
||||
/>
|
||||
</Link>
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import {
|
||||
AverageEventDurationChart,
|
||||
BookingKPICards,
|
||||
BookingStatusLineChart,
|
||||
LeastBookedTeamMembersTable,
|
||||
MostBookedTeamMembersTable,
|
||||
PopularEventsTable,
|
||||
} from "@calcom/features/insights/components";
|
||||
import { FiltersProvider } from "@calcom/features/insights/context/FiltersProvider";
|
||||
import { useFilterContext } from "@calcom/features/insights/context/provider";
|
||||
import { Filters } from "@calcom/features/insights/filters";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { UpgradeTip } from "@calcom/features/tips";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Button, ButtonGroup } from "@calcom/ui";
|
||||
import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
|
||||
|
||||
const Heading = () => {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
filter: { selectedTeamName },
|
||||
} = useFilterContext();
|
||||
return (
|
||||
<div className="min-w-52">
|
||||
<h3 className="font-cal max-w-28 sm:max-w-72 md:max-w-80 hidden truncate text-xl font-semibold tracking-wide text-black md:block xl:max-w-full">
|
||||
{t("analytics_for_organisation", {
|
||||
organisationName: selectedTeamName,
|
||||
})}
|
||||
</h3>
|
||||
<p className="hidden text-sm text-gray-500 md:block">{t("subtitle_analytics")}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function InsightsPage() {
|
||||
const { t } = useLocale();
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const features = [
|
||||
{
|
||||
icon: <FiUsers className="h-5 w-5" />,
|
||||
title: t("view_bookings_across"),
|
||||
description: t("view_bookings_across_description"),
|
||||
},
|
||||
{
|
||||
icon: <FiRefreshCcw className="h-5 w-5" />,
|
||||
title: t("identify_booking_trends"),
|
||||
description: t("identify_booking_trends_description"),
|
||||
},
|
||||
{
|
||||
icon: <FiUserPlus className="h-5 w-5" />,
|
||||
title: t("spot_popular_event_types"),
|
||||
description: t("spot_popular_event_types_description"),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Shell>
|
||||
<UpgradeTip
|
||||
title={t("make_informed_decisions")}
|
||||
description={t("make_informed_decisions_description")}
|
||||
features={features}
|
||||
background="/banners/insights.jpg"
|
||||
buttons={
|
||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||
<ButtonGroup>
|
||||
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
{t("create_team")}
|
||||
</Button>
|
||||
<Button color="secondary" href="https://go.cal.com/insights" target="_blank">
|
||||
{t("learn_more")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
}>
|
||||
{!user ? (
|
||||
<></>
|
||||
) : (
|
||||
<FiltersProvider>
|
||||
<div className="mb-4 mt-0 ml-auto flex w-full flex-wrap justify-between md:-mt-8">
|
||||
<Heading />
|
||||
<Filters />
|
||||
</div>
|
||||
<div className="mb-4 space-y-6">
|
||||
<BookingKPICards />
|
||||
|
||||
<BookingStatusLineChart />
|
||||
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<PopularEventsTable />
|
||||
|
||||
<AverageEventDurationChart />
|
||||
</div>
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-2">
|
||||
<MostBookedTeamMembersTable />
|
||||
<LeastBookedTeamMembersTable />
|
||||
</div>
|
||||
<small className="block text-center text-gray-600">
|
||||
{t("looking_for_more_analytics")}
|
||||
<a
|
||||
className="text-blue-500 hover:underline"
|
||||
href="mailto:updates@cal.com?subject=Feature%20Request%3A%20More%20Analytics&body=Hey%20Cal.com%20Team%2C%20I%20love%20the%20analytics%20page%20but%20I%20am%20looking%20for%20...">
|
||||
{" "}
|
||||
{t("contact_support")}
|
||||
</a>
|
||||
</small>
|
||||
</div>
|
||||
</FiltersProvider>
|
||||
)}
|
||||
</UpgradeTip>
|
||||
</Shell>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// If feature flag is disabled, return not found on getServerSideProps
|
||||
export const getServerSideProps = async () => {
|
||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||
const flags = await getFeatureFlagMap(prisma);
|
||||
|
||||
if (flags.insights === false) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,9 @@
|
||||
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
|
||||
|
||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||
|
||||
const FlagsPage = () => <FlagListingView />;
|
||||
|
||||
FlagsPage.getLayout = getLayout;
|
||||
|
||||
export default FlagsPage;
|
||||
@@ -1,6 +1,5 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import crypto from "crypto";
|
||||
import { signOut } from "next-auth/react";
|
||||
import type { BaseSyntheticEvent } from "react";
|
||||
import { useRef, useState } from "react";
|
||||
@@ -332,11 +331,6 @@ const ProfileForm = ({
|
||||
bio: z.string(),
|
||||
});
|
||||
|
||||
const emailMd5 = crypto
|
||||
.createHash("md5")
|
||||
.update(defaultValues.email || "example@example.com")
|
||||
.digest("hex");
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
defaultValues,
|
||||
resolver: zodResolver(profileFormSchema),
|
||||
@@ -356,7 +350,7 @@ const ProfileForm = ({
|
||||
name="avatar"
|
||||
render={({ field: { value } }) => (
|
||||
<>
|
||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
|
||||
<Avatar alt="" imageSrc={value} gravatarFallbackMd5="fallback" size="lg" />
|
||||
<div className="ltr:ml-4 rtl:mr-4">
|
||||
<ImageUploader
|
||||
target="avatar"
|
||||
|
||||
@@ -10,7 +10,7 @@ import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
@@ -113,7 +113,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
<>
|
||||
<div
|
||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(team.bio || "") }}
|
||||
dangerouslySetInnerHTML={{ __html: team.safeBio }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@@ -187,11 +187,18 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
...user,
|
||||
avatar: CAL_URL + "/" + user.username + "/avatar.png",
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(type.description),
|
||||
}));
|
||||
|
||||
const safeBio = markdownToSafeHTML(team.bio) || "";
|
||||
|
||||
const members = team.members.map((member) => {
|
||||
return { ...member, safeBio: markdownToSafeHTML(member.bio || "") };
|
||||
});
|
||||
|
||||
return {
|
||||
props: {
|
||||
team,
|
||||
team: { ...team, safeBio, members },
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -2,9 +2,9 @@ import type { GetServerSidePropsContext } from "next";
|
||||
|
||||
import type { LocationObject } from "@calcom/core/location";
|
||||
import { privacyFilteredLocations } from "@calcom/core/location";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getWorkingHours } from "@calcom/lib/availability";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -64,7 +64,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
avatar: true,
|
||||
username: true,
|
||||
timeZone: true,
|
||||
hideBranding: true,
|
||||
@@ -171,13 +170,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
hideBranding,
|
||||
timeZone,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
});
|
||||
|
||||
eventTypeObject.availability = [];
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
booking = await getBooking(prisma, rescheduleUid, getBookingFieldsWithSystemFields(eventTypeObject));
|
||||
booking = await getBooking(prisma, rescheduleUid);
|
||||
}
|
||||
|
||||
const weekStart = eventType.team?.members?.[0]?.user?.weekStart;
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { LocationObject } from "@calcom/app-store/locations";
|
||||
import { privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
@@ -121,13 +122,14 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
image: u.avatar,
|
||||
slug: u.username,
|
||||
})),
|
||||
descriptionAsSafeHTML: markdownToSafeHTML(eventType.description),
|
||||
};
|
||||
})[0];
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
const { rescheduleUid, bookingUid } = querySchema.parse(context.query);
|
||||
if (rescheduleUid || bookingUid) {
|
||||
booking = await getBooking(prisma, rescheduleUid || bookingUid || "", eventTypeObject.bookingFields);
|
||||
booking = await getBooking(prisma, rescheduleUid || bookingUid || "");
|
||||
}
|
||||
|
||||
// Checking if number of recurring event ocurrances is valid against event type configuration
|
||||
@@ -152,6 +154,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
eventType: eventTypeObject,
|
||||
recurringEventCount,
|
||||
booking,
|
||||
currentSlotBooking: null,
|
||||
isDynamicGroupBooking: false,
|
||||
hasHashedBookingLink: false,
|
||||
hashedLink: null,
|
||||
|
||||
@@ -119,7 +119,7 @@ test.describe("Event Types tests", () => {
|
||||
await page.locator("#location-select").click();
|
||||
await page.locator("text=In Person (Organizer Address)").click();
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(250);
|
||||
await page.waitForTimeout(1000);
|
||||
await page.locator('input[name="locationAddress"]').fill(inputText);
|
||||
await page.locator("[data-testid=display-location]").check();
|
||||
await page.locator("[data-testid=update-location]").click();
|
||||
|
||||
@@ -45,7 +45,7 @@ export const createEmbedsFixture = (page: Page) => {
|
||||
}
|
||||
api("on", {
|
||||
action: "*",
|
||||
callback: (e: any) => {
|
||||
callback: (e) => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
window.iframeReady = true; // Technically if there are multiple cal embeds, it can be set due to some other iframe. But it works for now. Improve it when it doesn't work
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Page } from "@playwright/test";
|
||||
import { expect } from "@playwright/test";
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { createServer } from "http";
|
||||
import noop from "lodash/noop";
|
||||
import { noop } from "lodash";
|
||||
|
||||
import { test } from "./fixtures";
|
||||
|
||||
|
||||
@@ -26,7 +26,11 @@ test.describe("Onboarding", () => {
|
||||
await page.locator("input[name=username]").fill("new user onboarding");
|
||||
await page.locator("input[name=name]").fill("new user 2");
|
||||
await page.locator("input[role=combobox]").click();
|
||||
await page.locator("text=Europe/London GMT +0:00").click();
|
||||
await page
|
||||
.locator("*")
|
||||
.filter({ hasText: /^Europe\/London/ })
|
||||
.first()
|
||||
.click();
|
||||
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
|
||||
@@ -97,11 +97,12 @@ test.describe("Reschedule Tests", async () => {
|
||||
|
||||
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
|
||||
|
||||
// NOTE: remove if old booking should not be deleted
|
||||
expect(await (await booking.self()).status).toBe(BookingStatus.CANCELLED);
|
||||
|
||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking.uid } });
|
||||
const rescheduledBooking = await prisma.booking.findFirst({ where: { uid: booking.uid } });
|
||||
|
||||
expect(newBooking).not.toBeNull();
|
||||
expect(rescheduledBooking).toBeNull();
|
||||
|
||||
await prisma.booking.delete({ where: { id: newBooking?.id } });
|
||||
});
|
||||
|
||||
@@ -148,7 +149,6 @@ test.describe("Reschedule Tests", async () => {
|
||||
});
|
||||
|
||||
await expect(page).toHaveURL(/.*payment/);
|
||||
await payment.delete();
|
||||
});
|
||||
|
||||
test("Paid rescheduling should go to success page", async ({ page, users, bookings, payments }) => {
|
||||
@@ -172,8 +172,6 @@ test.describe("Reschedule Tests", async () => {
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page).toHaveURL(/.*booking/);
|
||||
|
||||
await payment.delete();
|
||||
});
|
||||
|
||||
test("Opt in event should be PENDING when rescheduled by USER", async ({ page, users, bookings }) => {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 155 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 204 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 173 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 56 KiB |
@@ -111,7 +111,7 @@
|
||||
"hidden_team_owner_message": "You need a pro account to use teams, you are hidden until you upgrade.",
|
||||
"link_expires": "p.s. It expires in {{expiresIn}} hours.",
|
||||
"upgrade_to_per_seat": "Upgrade to Per-Seat",
|
||||
"seat_options_doesnt_support_confirmation":"Seats option doesn't support confirmation requirement",
|
||||
"seat_options_doesnt_support_confirmation": "Seats option doesn't support confirmation requirement",
|
||||
"team_upgrade_seats_details": "Of the {{memberCount}} members in your team, {{unpaidCount}} seat(s) are unpaid. At ${{seatPrice}}/month per seat the estimated total cost of your membership is ${{totalCost}}/month.",
|
||||
"team_upgrade_banner_description": "Thank you for trialing our new team plan. We noticed your team \"{{teamName}}\" needs to be upgraded.",
|
||||
"team_upgrade_banner_action": "Upgrade here",
|
||||
@@ -433,7 +433,7 @@
|
||||
"password_hint_min": "Minimum 7 characters long",
|
||||
"password_hint_admin_min": "Minimum 15 characters long",
|
||||
"password_hint_num": "Contain at least 1 number",
|
||||
"max_limit_allowed_hint":"Must be {{limit}} or fewer characters long",
|
||||
"max_limit_allowed_hint": "Must be {{limit}} or fewer characters long",
|
||||
"invalid_password_hint": "The password must be a minimum of {{passwordLength}} characters long containing at least one number and have a mixture of uppercase and lowercase letters",
|
||||
"incorrect_password": "Password is incorrect.",
|
||||
"incorrect_username_password": "Username or password is incorrect.",
|
||||
@@ -863,7 +863,7 @@
|
||||
"add_new_calendar": "Add a new calendar",
|
||||
"set_calendar": "Set where to add new events to when you're booked.",
|
||||
"delete_schedule": "Delete schedule",
|
||||
"delete_schedule_description":"Deleting a schedule will remove it from all event types. This action cannot be undone.",
|
||||
"delete_schedule_description": "Deleting a schedule will remove it from all event types. This action cannot be undone.",
|
||||
"schedule_created_successfully": "{{scheduleName}} schedule created successfully",
|
||||
"availability_updated_successfully": "{{scheduleName}} schedule updated successfully",
|
||||
"schedule_deleted_successfully": "Schedule deleted successfully",
|
||||
@@ -1218,6 +1218,7 @@
|
||||
"impersonation": "Impersonation",
|
||||
"impersonation_description": "Settings to manage user impersonation",
|
||||
"users": "Users",
|
||||
"user": "User",
|
||||
"profile_description": "Manage settings for your {{appName}} profile",
|
||||
"users_description": "Here you can find a list of all users",
|
||||
"users_listing": "User listing",
|
||||
@@ -1608,6 +1609,7 @@
|
||||
"default_app_link_title": "Set a default app link",
|
||||
"default_app_link_description": "Setting a default app link allows all newly created event types to use the app link you set.",
|
||||
"change_default_conferencing_app": "Set as default",
|
||||
"organizer_default_conferencing_app": "Organizer's default app",
|
||||
"under_maintenance": "Down for maintenance",
|
||||
"under_maintenance_description": "The {{appName}} team are performing scheduled maintenance. If you have any questions, please contact support.",
|
||||
"event_type_seats": "{{numberOfSeats}} seats",
|
||||
@@ -1631,22 +1633,30 @@
|
||||
"a_routing_form": "A Routing Form",
|
||||
"form_description_placeholder": "Form Description",
|
||||
"keep_me_connected_with_form": "Keep me connected with the form",
|
||||
"fields_in_form_duplicated":"Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.",
|
||||
"fields_in_form_duplicated": "Any changes in Router and Fields of the form being duplicated, would reflect in the duplicate.",
|
||||
"form_deleted": "Form deleted",
|
||||
"delete_form": "Are you sure you want to delete this form?",
|
||||
"delete_form_action": "Yes, delete Form",
|
||||
"delete_form_confirmation":"Anyone who you've shared the link with will no longer be able to access it.",
|
||||
"delete_form_confirmation_2":"All associated responses will be deleted.",
|
||||
"delete_form_confirmation": "Anyone who you've shared the link with will no longer be able to access it.",
|
||||
"delete_form_confirmation_2": "All associated responses will be deleted.",
|
||||
"typeform_redirect_url_copied": "Typeform Redirect URL copied! You can go and set the URL in Typeform form.",
|
||||
"modifications_in_fields_warning": "Modifications in fields and routes of following forms will be reflected in this form.",
|
||||
"connected_forms": "Connected Forms",
|
||||
"form_modifications_warning": "Following forms would be affected when you modify fields or routes here.",
|
||||
"responses_collection_waiting_description": "Wait for some time for responses to be collected. You can go and submit the form yourself as well.",
|
||||
"this_is_what_your_users_would_see":"This is what your users would see",
|
||||
"this_is_what_your_users_would_see": "This is what your users would see",
|
||||
"identifies_name_field": "Identifies field by this name.",
|
||||
"add_1_option_per_line": "Add 1 option per line",
|
||||
"select_a_router": "Select a router",
|
||||
"add_a_new_route": "Add a new Route",
|
||||
"make_informed_decisions": "Make informed decisions with Insights",
|
||||
"make_informed_decisions_description": "Our Insights dashboard surfaces all activity across your team and shows you trends that enable better team scheduling and decision making.",
|
||||
"view_bookings_across": "View bookings across all members",
|
||||
"view_bookings_across_description": "See who’s receiving the most bookings and ensure the best distribution across your team",
|
||||
"identify_booking_trends": "Identify booking trends",
|
||||
"identify_booking_trends_description": "See what times of the week and what times during the day are popular for your bookers",
|
||||
"spot_popular_event_types": "Spot popular event types",
|
||||
"spot_popular_event_types_description": "See which of your event types are receiving the most clicks and bookings",
|
||||
"no_responses_yet": "No responses yet",
|
||||
"this_will_be_the_placeholder": "This will be the placeholder",
|
||||
"this_meeting_has_not_started_yet": "This meeting has not started yet",
|
||||
@@ -1657,8 +1667,30 @@
|
||||
"verification_code": "Verification code",
|
||||
"can_you_try_again": "Can you try again with a different time?",
|
||||
"verify": "Verify",
|
||||
"invalid_event_name_variables":"There is an invalid variable in your event name",
|
||||
"invalid_event_name_variables": "There is an invalid variable in your event name",
|
||||
"select_all": "Select All",
|
||||
"default_conferencing_bulk_title": "Bulk update existing event types",
|
||||
"default_conferencing_bulk_description": "Update the locations for the selected event types"
|
||||
"requires_at_least_one_schedule": "You are required to have at least one schedule",
|
||||
"default_conferencing_bulk_description": "Update the locations for the selected event types",
|
||||
"looking_for_more_analytics": "Looking for more analytics?",
|
||||
"add_filter": "Add filter",
|
||||
"select_user": "Select User",
|
||||
"select_event_type": "Select Event Type",
|
||||
"select_date_range": "Select Date Range",
|
||||
"popular_events": "Popular Events",
|
||||
"no_event_types_found": "No event types found",
|
||||
"average_event_duration": "Average Event Duration",
|
||||
"most_booked_members": "Most Booked Members",
|
||||
"least_booked_members": "Least Booked Members",
|
||||
"events_created": "Events Created",
|
||||
"events_completed": "Events Completed",
|
||||
"events_cancelled": "Events Cancelled",
|
||||
"events_rescheduled": "Events Rescheduled",
|
||||
"from_last_period": "from last period",
|
||||
"from_to_date_period": "From: {{startDate}} To: {{endDate}}",
|
||||
"analytics_for_organisation": "Insights for {{organisationName}}",
|
||||
"subtitle_analytics": "Learn more about your team's activity",
|
||||
"event_trends": "Event Trends",
|
||||
"clear_filters": "Clear Filters",
|
||||
"insights": "Insights"
|
||||
}
|
||||
|
||||
@@ -1213,6 +1213,7 @@
|
||||
"impersonation": "Suplantación",
|
||||
"impersonation_description": "Configuración para administrar la suplantación del usuario",
|
||||
"users": "Usuarios",
|
||||
"user": "Usuario",
|
||||
"profile_description": "Administra los ajustes para tu perfil de {{appName}}",
|
||||
"users_description": "Aquí puede encontrar una lista de todos los usuarios",
|
||||
"users_listing": "Lista de usuarios",
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 46 KiB |
@@ -29,7 +29,6 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
|
||||
ctx: {
|
||||
prisma,
|
||||
session: null,
|
||||
user: null,
|
||||
locale,
|
||||
i18n: _i18n,
|
||||
},
|
||||
|
||||
@@ -497,3 +497,5 @@ hr {
|
||||
::-webkit-search-cancel-button {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@ const base = require("@calcom/config/tailwind-preset");
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
module.exports = {
|
||||
...base,
|
||||
content: [...base.content],
|
||||
content: [...base.content, "../../node_modules/@tremor/**/*.{js,ts,jsx,tsx}"],
|
||||
};
|
||||
|
||||
+3
-1
@@ -70,6 +70,7 @@
|
||||
"test-playwright": "yarn playwright test --config=playwright.config.ts",
|
||||
"test": "jest",
|
||||
"type-check": "turbo run type-check",
|
||||
"type-check:ci": "turbo run type-check:ci --log-prefix=none",
|
||||
"web": "yarn workspace @calcom/web"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -86,8 +87,9 @@
|
||||
"jest-watch-select-projects": "^2.0.0",
|
||||
"jest-watch-typeahead": "^2.0.0",
|
||||
"lint-staged": "^12.5.0",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier": "^2.8.6",
|
||||
"ts-jest": "^28.0.8",
|
||||
"tsc-absolute": "^1.0.0",
|
||||
"typescript": "^4.9.4"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
|
||||
import seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
|
||||
import type seedAppStoreConfig from "@calcom/prisma/seed-app-store.config.json";
|
||||
|
||||
import { APP_STORE_PATH, TEMPLATES_PATH } from "./constants";
|
||||
import execSync from "./utils/execSync";
|
||||
@@ -127,6 +127,8 @@ export const BaseAppFork = {
|
||||
.replace(/_DESCRIPTION_/g, description)
|
||||
.replace(/_APP_DIR_/g, slug)
|
||||
);
|
||||
// New monorepo package has been added, so we need to run yarn again
|
||||
await execSync("yarn");
|
||||
},
|
||||
|
||||
delete: async function ({ slug, isTemplate }: { slug: string; isTemplate: boolean }) {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
Don't modify this file manually.
|
||||
**/
|
||||
import dynamic from "next/dynamic";
|
||||
|
||||
export const InstallAppButtonMap = {
|
||||
applecalendar: dynamic(() => import("./applecalendar/components/InstallAppButton")),
|
||||
around: dynamic(() => import("./around/components/InstallAppButton")),
|
||||
|
||||
@@ -24,7 +24,6 @@ import { appKeysSchema as vital_zod_ts } from "./vital/zod";
|
||||
import { appKeysSchema as wordpress_zod_ts } from "./wordpress/zod";
|
||||
import { appKeysSchema as zapier_zod_ts } from "./zapier/zod";
|
||||
import { appKeysSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
|
||||
|
||||
export const appKeysSchemas = {
|
||||
dailyvideo: dailyvideo_zod_ts,
|
||||
fathom: fathom_zod_ts,
|
||||
|
||||
@@ -58,7 +58,6 @@ import wordpress_config_json from "./wordpress/config.json";
|
||||
import { metadata as zapier__metadata_ts } from "./zapier/_metadata";
|
||||
import zohocrm_config_json from "./zohocrm/config.json";
|
||||
import { metadata as zoomvideo__metadata_ts } from "./zoomvideo/_metadata";
|
||||
|
||||
export const appStoreMetadata = {
|
||||
amie: amie_config_json,
|
||||
applecalendar: applecalendar__metadata_ts,
|
||||
|
||||
@@ -24,7 +24,6 @@ import { appDataSchema as vital_zod_ts } from "./vital/zod";
|
||||
import { appDataSchema as wordpress_zod_ts } from "./wordpress/zod";
|
||||
import { appDataSchema as zapier_zod_ts } from "./zapier/zod";
|
||||
import { appDataSchema as zoomvideo_zod_ts } from "./zoomvideo/zod";
|
||||
|
||||
export const appDataSchemas = {
|
||||
dailyvideo: dailyvideo_zod_ts,
|
||||
fathom: fathom_zod_ts,
|
||||
|
||||
@@ -10,6 +10,7 @@ items:
|
||||
{DESCRIPTION}
|
||||
|
||||
### Cron's most valuable features
|
||||
|
||||
<!-- From "FAQs: What are Cron’s most valuable features?"
|
||||
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
|
||||
|
||||
|
||||
@@ -133,6 +133,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
calendarId: selectedCalendar,
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
sendUpdates: "none",
|
||||
},
|
||||
function (error, event) {
|
||||
if (error || !event?.data) {
|
||||
@@ -234,14 +235,13 @@ export default class GoogleCalendarService implements Calendar {
|
||||
calendarId: selectedCalendar,
|
||||
eventId: uid,
|
||||
sendNotifications: true,
|
||||
sendUpdates: "all",
|
||||
sendUpdates: "none",
|
||||
requestBody: payload,
|
||||
conferenceDataVersion: 1,
|
||||
},
|
||||
function (err, evt) {
|
||||
if (err) {
|
||||
console.error("There was an error contacting google calendar service: ", err);
|
||||
|
||||
return reject(err);
|
||||
}
|
||||
|
||||
@@ -293,7 +293,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
calendarId: calendarId ? calendarId : defaultCalendarId,
|
||||
eventId: uid,
|
||||
sendNotifications: false,
|
||||
sendUpdates: "all",
|
||||
sendUpdates: "none",
|
||||
},
|
||||
function (err: GoogleCalError | null, event) {
|
||||
if (err) {
|
||||
|
||||
@@ -12,13 +12,20 @@ export type DefaultEventLocationType = {
|
||||
type: DefaultEventLocationTypeEnum;
|
||||
label: string;
|
||||
messageForOrganizer: string;
|
||||
category: "in person" | "other" | "phone";
|
||||
category: "in person" | "conferencing" | "other" | "phone";
|
||||
|
||||
iconUrl: string;
|
||||
urlRegExp?: string;
|
||||
// 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";
|
||||
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "phone";
|
||||
variable:
|
||||
| "locationType"
|
||||
| "locationAddress"
|
||||
| "address"
|
||||
| "locationLink"
|
||||
| "locationPhoneNumber"
|
||||
| "phone"
|
||||
| "hostDefault";
|
||||
defaultValueVariable: "address" | "attendeeAddress" | "link" | "hostPhoneNumber" | "hostDefault" | "phone";
|
||||
} & (
|
||||
| {
|
||||
organizerInputType: "phone" | "text" | null;
|
||||
@@ -60,6 +67,7 @@ export enum DefaultEventLocationTypeEnum {
|
||||
*/
|
||||
UserPhone = "userPhone",
|
||||
Link = "link",
|
||||
Conferencing = "conferencing",
|
||||
}
|
||||
|
||||
export const defaultLocations: DefaultEventLocationType[] = [
|
||||
@@ -88,6 +96,17 @@ export const defaultLocations: DefaultEventLocationType[] = [
|
||||
iconUrl: "/map-pin.svg",
|
||||
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,
|
||||
type: DefaultEventLocationTypeEnum.Link,
|
||||
@@ -130,7 +149,9 @@ export const defaultLocations: DefaultEventLocationType[] = [
|
||||
export type LocationObject = {
|
||||
type: string;
|
||||
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
|
||||
export type BookingLocationValue = string;
|
||||
|
||||
@@ -219,7 +219,7 @@ export default class Office365CalendarService implements Calendar {
|
||||
};
|
||||
const o365AuthCredentials = credential.key as O365AuthCredentials;
|
||||
|
||||
const refreshAccessToken = async (refreshToken: string) => {
|
||||
const refreshAccessToken = async (o365AuthCredentials: O365AuthCredentials) => {
|
||||
const { client_id, client_secret } = await getOfficeAppKeys();
|
||||
const response = await fetch("https://login.microsoftonline.com/common/oauth2/v2.0/token", {
|
||||
method: "POST",
|
||||
@@ -227,12 +227,22 @@ export default class Office365CalendarService implements Calendar {
|
||||
body: new URLSearchParams({
|
||||
scope: "User.Read Calendars.Read Calendars.ReadWrite",
|
||||
client_id,
|
||||
refresh_token: refreshToken,
|
||||
refresh_token: o365AuthCredentials.refresh_token,
|
||||
grant_type: "refresh_token",
|
||||
client_secret,
|
||||
}),
|
||||
});
|
||||
const o365AuthCredentials = refreshTokenResponseSchema.parse(await handleErrorsJson(response));
|
||||
const responseJson = await handleErrorsJson(response);
|
||||
const tokenResponse = refreshTokenResponseSchema.safeParse(responseJson);
|
||||
o365AuthCredentials = { ...o365AuthCredentials, ...(tokenResponse.success && tokenResponse.data) };
|
||||
if (!tokenResponse.success) {
|
||||
console.error(
|
||||
"Outlook error grabbing new tokens ~ zodError:",
|
||||
tokenResponse.error,
|
||||
"MS response:",
|
||||
responseJson
|
||||
);
|
||||
}
|
||||
await prisma.credential.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
@@ -246,9 +256,10 @@ export default class Office365CalendarService implements Calendar {
|
||||
|
||||
return {
|
||||
getToken: () =>
|
||||
refreshTokenResponseSchema.safeParse(o365AuthCredentials).success &&
|
||||
!isExpired(o365AuthCredentials.expires_in)
|
||||
? Promise.resolve(o365AuthCredentials.access_token)
|
||||
: refreshAccessToken(o365AuthCredentials.refresh_token),
|
||||
: refreshAccessToken(o365AuthCredentials),
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||
"lint": "eslint . --ext .ts,.js,.tsx,.jsx",
|
||||
"lint:fix": "eslint . --ext .ts,.js,.tsx,.jsx --fix",
|
||||
"lint:report": "eslint . --format json --output-file ../../lint-results/app-store.json"
|
||||
|
||||
@@ -99,23 +99,18 @@ export default function RoutingForms({
|
||||
CTA={hasPaidPlan && <NewFormButton />}
|
||||
subtitle={t("routing_forms_description")}>
|
||||
<UpgradeTip
|
||||
dark
|
||||
title={t("teams_plan_required")}
|
||||
description={t("routing_forms_are_a_great_way")}
|
||||
features={features}
|
||||
background="/routing-form-banner-background.jpg"
|
||||
background="/banners/routing-forms.jpg"
|
||||
isParentLoading={isLoading && <SkeletonLoaderTeamList />}
|
||||
buttons={
|
||||
<div className="space-y-2 rtl:space-x-reverse sm:space-x-2">
|
||||
<ButtonGroup>
|
||||
<Button color="secondary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
<Button color="primary" href={`${WEBAPP_URL}/settings/teams/new`}>
|
||||
{t("upgrade")}
|
||||
</Button>
|
||||
<Button
|
||||
color="minimal"
|
||||
className="!bg-transparent text-white opacity-50 hover:opacity-100"
|
||||
href="https://go.cal.com/teams-video"
|
||||
target="_blank">
|
||||
<Button color="minimal" href="https://go.cal.com/teams-video" target="_blank">
|
||||
{t("learn_more")}
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"type": "integrations:whereby_video",
|
||||
"label": "Whereby Video",
|
||||
"organizerInputPlaceholder": "https://www.whereby.com/cal",
|
||||
"urlRegExp": "^http(s)?:\\/\\/(www\\.)?(team.)?whereby.com\\/[a-zA-Z0-9]*"
|
||||
"urlRegExp": "^(?:https?://)?(?:(?!.*-\\.)(?:\\w+(-\\w+)*\\.))*whereby\\.com(/[\\w\\-._~:?#\\[\\]@!$&'()*+,;%=]+)*$"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/line-clamp": "^0.4.0",
|
||||
"@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/parser": "^5.52.0",
|
||||
"eslint": "^8.34.0",
|
||||
"eslint-plugin-unused-imports": "^2.0.0",
|
||||
"prettier": "^2.8.4",
|
||||
"prettier-plugin-tailwindcss": "^0.1.13",
|
||||
"prettier": "^2.8.6",
|
||||
"prettier-plugin-tailwindcss": "^0.2.5",
|
||||
"tailwind-scrollbar": "^2.0.1",
|
||||
"tailwindcss": "^3.2.1",
|
||||
"typescript": "^4.9.4"
|
||||
|
||||
@@ -16,7 +16,14 @@ module.exports = {
|
||||
"^[./]",
|
||||
],
|
||||
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: [
|
||||
{
|
||||
files: ["apps/website/lib/utils/wordlist/wordlist.ts"],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { SelectedCalendar } from "@prisma/client";
|
||||
import _ from "lodash";
|
||||
import { sortBy } from "lodash";
|
||||
import * as process from "process";
|
||||
|
||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
||||
@@ -54,8 +54,8 @@ export const getConnectedCalendars = async (
|
||||
};
|
||||
}
|
||||
const cals = await calendar.listCalendars();
|
||||
const calendars = _(cals)
|
||||
.map((cal) => {
|
||||
const calendars = sortBy(
|
||||
cals.map((cal) => {
|
||||
if (cal.externalId === destinationCalendarExternalId) destinationCalendar = cal;
|
||||
return {
|
||||
...cal,
|
||||
@@ -64,9 +64,9 @@ export const getConnectedCalendars = async (
|
||||
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
||||
credentialId,
|
||||
};
|
||||
})
|
||||
.sortBy(["primary"])
|
||||
.value();
|
||||
}),
|
||||
["primary"]
|
||||
);
|
||||
const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined);
|
||||
if (!primary) {
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { DestinationCalendar, Booking } from "@prisma/client";
|
||||
import { cloneDeep } from "lodash";
|
||||
import merge from "lodash/merge";
|
||||
import { cloneDeep, merge } from "lodash";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
import type { z } from "zod";
|
||||
|
||||
|
||||
@@ -1,31 +1,6 @@
|
||||
/* eslint-disable @calcom/eslint/deprecated-imports */
|
||||
import dayjs from "dayjs";
|
||||
import dayjsBusinessTime from "dayjs-business-days2";
|
||||
import "dayjs/locale/ar";
|
||||
import "dayjs/locale/bg";
|
||||
import "dayjs/locale/cs";
|
||||
import "dayjs/locale/de";
|
||||
import "dayjs/locale/es";
|
||||
import "dayjs/locale/es-mx";
|
||||
import "dayjs/locale/fr";
|
||||
import "dayjs/locale/he";
|
||||
import "dayjs/locale/hu";
|
||||
import "dayjs/locale/it";
|
||||
import "dayjs/locale/ja";
|
||||
import "dayjs/locale/ko";
|
||||
import "dayjs/locale/nl";
|
||||
import "dayjs/locale/pl";
|
||||
import "dayjs/locale/pt";
|
||||
import "dayjs/locale/pt-br";
|
||||
import "dayjs/locale/ro";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/sr";
|
||||
import "dayjs/locale/sv";
|
||||
import "dayjs/locale/tr";
|
||||
import "dayjs/locale/uk";
|
||||
import "dayjs/locale/vi";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import "dayjs/locale/zh-tw";
|
||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||
import isBetween from "dayjs/plugin/isBetween";
|
||||
import isToday from "dayjs/plugin/isToday";
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import "dayjs/locale/ar";
|
||||
import "dayjs/locale/bg";
|
||||
import "dayjs/locale/cs";
|
||||
import "dayjs/locale/de";
|
||||
import "dayjs/locale/es";
|
||||
import "dayjs/locale/es-mx";
|
||||
import "dayjs/locale/fr";
|
||||
import "dayjs/locale/he";
|
||||
import "dayjs/locale/hu";
|
||||
import "dayjs/locale/it";
|
||||
import "dayjs/locale/ja";
|
||||
import "dayjs/locale/ko";
|
||||
import "dayjs/locale/nl";
|
||||
import "dayjs/locale/pl";
|
||||
import "dayjs/locale/pt";
|
||||
import "dayjs/locale/pt-br";
|
||||
import "dayjs/locale/ro";
|
||||
import "dayjs/locale/ru";
|
||||
import "dayjs/locale/sr";
|
||||
import "dayjs/locale/sv";
|
||||
import "dayjs/locale/tr";
|
||||
import "dayjs/locale/uk";
|
||||
import "dayjs/locale/vi";
|
||||
import "dayjs/locale/zh-cn";
|
||||
import "dayjs/locale/zh-tw";
|
||||
@@ -1,3 +1,5 @@
|
||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||
|
||||
const Spacer = () => <p style={{ height: 6 }} />;
|
||||
|
||||
export const Info = (props: {
|
||||
@@ -6,8 +8,14 @@ export const Info = (props: {
|
||||
extraInfo?: React.ReactNode;
|
||||
withSpacer?: boolean;
|
||||
lineThrough?: boolean;
|
||||
formatted?: boolean;
|
||||
}) => {
|
||||
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 (
|
||||
<>
|
||||
{props.withSpacer && <Spacer />}
|
||||
@@ -21,7 +29,18 @@ export const Info = (props: {
|
||||
whiteSpace: "pre-wrap",
|
||||
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>
|
||||
{props.extraInfo}
|
||||
</div>
|
||||
|
||||
@@ -2,6 +2,8 @@ import type { TFunction } from "next-i18next";
|
||||
import { RRule } from "rrule";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
// TODO: Use browser locale, implement Intl in Dayjs maybe?
|
||||
import "@calcom/dayjs/locales";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
||||
|
||||
@@ -76,7 +76,7 @@ export const BaseScheduledEmail = (
|
||||
<WhenInfo calEvent={props.calEvent} t={t} timeZone={timeZone} />
|
||||
<WhoInfo 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 />
|
||||
{props.includeAppsStatus && <AppsStatus calEvent={props.calEvent} t={t} />}
|
||||
<UserFieldsResponses calEvent={props.calEvent} />
|
||||
|
||||
@@ -4,8 +4,10 @@ import { z } from "zod";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getFeatureFlagMap } from "@calcom/features/flags/server/utils";
|
||||
import { getErrorFromUnknown } from "@calcom/lib/errors";
|
||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
declare let global: {
|
||||
E2E_EMAILS?: Record<string, unknown>[];
|
||||
@@ -29,7 +31,13 @@ export default class BaseEmail {
|
||||
protected getNodeMailerPayload(): Record<string, unknown> {
|
||||
return {};
|
||||
}
|
||||
public sendEmail() {
|
||||
public async sendEmail() {
|
||||
const featureFlags = await getFeatureFlagMap(prisma);
|
||||
/** If email kill switch exists and is active, we prevent emails being sent. */
|
||||
if (featureFlags.emails) {
|
||||
console.warn("Skipped Sending Email due to active Kill Switch");
|
||||
return new Promise((r) => r("Skipped Sending Email due to active Kill Switch"));
|
||||
}
|
||||
if (process.env.NEXT_PUBLIC_IS_E2E) {
|
||||
global.E2E_EMAILS = global.E2E_EMAILS || [];
|
||||
global.E2E_EMAILS.push(this.getNodeMailerPayload());
|
||||
|
||||
@@ -4,6 +4,16 @@
|
||||
<!-- <link rel="prerender" href="http://localhost:3000/free"> -->
|
||||
<!-- <script src="./src/embed.ts" type="module"></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")) {
|
||||
document.write('<meta name="viewport" content="width=device-width"/>');
|
||||
}
|
||||
@@ -90,6 +100,7 @@
|
||||
</div>
|
||||
<span style="display: block;" ><a href="?only=all">Render All embeds together</a><small> - It would be slow to load</small></span>
|
||||
<div>
|
||||
<a href="?only=ns:floatingButton">Floating Popup</a>
|
||||
|
||||
<h2>Popup Examples</h2>
|
||||
<button data-cal-namespace="popupAutoTheme" data-cal-link="free">Book with Free User[Auto Theme]</button>
|
||||
@@ -140,12 +151,12 @@
|
||||
Align Center
|
||||
</button>
|
||||
<button onclick="(function () {Cal.ns.second('ui', {styles:{enabledDateButton: {
|
||||
backgroundColor: '#D3D3D3',
|
||||
backgroundColor: generateRandomHexColor(),
|
||||
},
|
||||
disabledDateButton: {
|
||||
backgroundColor: 'lightslategray',
|
||||
backgroundColor: generateRandomHexColor(),
|
||||
},}})})()">
|
||||
Change Date Button Color
|
||||
Change 'enabledDateButton` and `disabledDateButton` background Color
|
||||
</button>
|
||||
|
||||
</div>
|
||||
@@ -189,318 +200,6 @@
|
||||
<div class="place">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<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>
|
||||
<script src="./playground.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"dev": "yarn tailwind && run-p 'tailwind --watch' 'buildWatchAndServer'",
|
||||
"dev-real": "vite dev --port 3100",
|
||||
"type-check": "tsc --pretty --noEmit",
|
||||
"type-check:ci": "tsc-absolute --pretty --noEmit",
|
||||
"lint": "eslint --ext .ts,.js src",
|
||||
"lint:fix": "eslint --ext .ts,.js src --fix",
|
||||
"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";
|
||||
|
||||
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 {
|
||||
static updatedClassString(position: string, classString: string) {
|
||||
return [
|
||||
@@ -8,66 +39,69 @@ export class FloatingButton extends HTMLElement {
|
||||
].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
|
||||
//@ts-ignore
|
||||
static get observedAttributes() {
|
||||
return [
|
||||
"data-button-text",
|
||||
"data-hide-button-icon",
|
||||
"data-button-position",
|
||||
"data-button-color",
|
||||
"data-button-text-color",
|
||||
];
|
||||
return dataAttributes;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
||||
attributeChangedCallback(name: DataAttributes, oldValue: string, newValue: string) {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("#button");
|
||||
const buttonWrapperEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
||||
const buttonIconEl = this.shadowRoot?.querySelector<HTMLElement>("#button-icon");
|
||||
|
||||
if (!buttonEl) {
|
||||
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") {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("#button");
|
||||
if (!buttonEl) {
|
||||
throw new Error("Button not found");
|
||||
}
|
||||
buttonEl.textContent = newValue;
|
||||
} 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";
|
||||
} else if (name === "data-button-position") {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
||||
if (!buttonEl) {
|
||||
throw new Error("Button not found");
|
||||
}
|
||||
buttonEl.className = FloatingButton.updatedClassString(newValue, buttonEl.className);
|
||||
buttonWrapperEl.className = FloatingButton.updatedClassString(newValue, buttonWrapperEl.className);
|
||||
} else if (name === "data-button-color") {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
||||
if (!buttonEl) {
|
||||
throw new Error("Button not found");
|
||||
}
|
||||
buttonEl.style.backgroundColor = newValue;
|
||||
buttonWrapperEl.style.backgroundColor = newValue;
|
||||
} else if (name === "data-button-text-color") {
|
||||
const buttonEl = this.shadowRoot?.querySelector<HTMLElement>("button");
|
||||
if (!buttonEl) {
|
||||
throw new Error("Button not found");
|
||||
}
|
||||
buttonEl.style.color = newValue;
|
||||
buttonWrapperEl.style.color = newValue;
|
||||
} else {
|
||||
console.log("Unknown attribute changed", name, oldValue, newValue);
|
||||
}
|
||||
}
|
||||
|
||||
assertHasShadowRoot(): asserts this is HTMLElement & { shadowRoot: ShadowRoot } {
|
||||
if (!this.shadowRoot) {
|
||||
throw new Error("No shadow root");
|
||||
}
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const buttonText = this.dataset["buttonText"];
|
||||
const buttonPosition = this.dataset["buttonPosition"];
|
||||
const buttonColor = this.dataset["buttonColor"];
|
||||
const buttonTextColor = this.dataset["buttonTextColor"];
|
||||
const dataset = this.dataset as FloatingButtonDataset;
|
||||
const buttonText = dataset["buttonText"];
|
||||
const buttonPosition = dataset["buttonPosition"];
|
||||
const buttonColor = dataset["buttonColor"];
|
||||
const buttonTextColor = dataset["buttonTextColor"];
|
||||
|
||||
//TODO: Logic is duplicated over HTML generation and attribute change, keep it at one place
|
||||
const buttonHtml = `<style>${window.Cal!.__css}</style> ${getFloatingButtonHtml({
|
||||
buttonText: buttonText!,
|
||||
buttonClasses: [FloatingButton.updatedClassString(buttonPosition!, "")],
|
||||
buttonColor: buttonColor!,
|
||||
buttonTextColor: buttonTextColor!,
|
||||
const buttonHtml = `<style>${window.Cal.__css}</style> ${getFloatingButtonHtml({
|
||||
buttonText: buttonText,
|
||||
buttonClasses: [FloatingButton.updatedClassString(buttonPosition, "")],
|
||||
buttonColor: buttonColor,
|
||||
buttonTextColor: buttonTextColor,
|
||||
})}`;
|
||||
|
||||
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 inlineHtml from "./inlineHtml";
|
||||
|
||||
export class Inline extends HTMLElement {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-ignore
|
||||
static get observedAttributes() {
|
||||
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) {
|
||||
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 (newValue == "done") {
|
||||
this.shadowRoot!.querySelector<HTMLElement>(".loader")!.style.display = "none";
|
||||
loaderEl.style.display = "none";
|
||||
} else if (newValue === "failed") {
|
||||
this.shadowRoot!.querySelector<HTMLElement>(".loader")!.style.display = "none";
|
||||
this.shadowRoot!.querySelector<HTMLElement>("#error")!.style.display = "block";
|
||||
this.shadowRoot!.querySelector<HTMLElement>("slot")!.style.visibility = "hidden";
|
||||
loaderEl.style.display = "none";
|
||||
slotEl.style.visibility = "hidden";
|
||||
errorEl.style.display = "block";
|
||||
const errorString = getErrorString(this.dataset.errorCode);
|
||||
this.shadowRoot!.querySelector<HTMLElement>("#error")!.innerText = errorString;
|
||||
errorEl.innerText = errorString;
|
||||
}
|
||||
}
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.shadowRoot!.innerHTML = `<style>${
|
||||
window.Cal!.__css
|
||||
}</style><style>${loaderCss}</style>${inlineHtml}`;
|
||||
this.assertHasShadowRoot();
|
||||
this.shadowRoot.innerHTML = `<style>${window.Cal.__css}</style><style>${loaderCss}</style>${inlineHtml}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export class ModalBox extends HTMLElement {
|
||||
|
||||
constructor() {
|
||||
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" });
|
||||
ModalBox.htmlOverflow = document.body.style.overflow;
|
||||
document.body.style.overflow = "hidden";
|
||||
|
||||
@@ -2,18 +2,40 @@ import { useRouter } from "next/router";
|
||||
import type { CSSProperties } from "react";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
import type { Message } from "./embed";
|
||||
import { sdkActionManager } from "./sdk-event";
|
||||
|
||||
export interface UiConfig {
|
||||
export type UiConfig = {
|
||||
hideEventTypeDetails?: boolean;
|
||||
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 {
|
||||
interface Window {
|
||||
CalEmbed: {
|
||||
__logQueue?: any[];
|
||||
embedStore: any;
|
||||
__logQueue?: unknown[];
|
||||
embedStore: typeof embedStore;
|
||||
};
|
||||
CalComPageStatus: string;
|
||||
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;
|
||||
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) {
|
||||
// https://adpiler.com/blog/the-full-solution-why-do-animations-run-slower-in-safari/
|
||||
return setTimeout(fn, 50);
|
||||
@@ -66,7 +66,7 @@ function runAsap(fn: (...arg: any) => void) {
|
||||
return requestAnimationFrame(fn);
|
||||
}
|
||||
|
||||
function log(...args: any[]) {
|
||||
function log(...args: unknown[]) {
|
||||
if (isBrowser) {
|
||||
const namespace = getNamespace();
|
||||
|
||||
@@ -95,7 +95,7 @@ interface EmbedStyles {
|
||||
}
|
||||
interface EmbedNonStylesConfig {
|
||||
/** Default would be center */
|
||||
align: "left";
|
||||
align?: "left";
|
||||
branding?: {
|
||||
brandColor?: string;
|
||||
lightColor?: string;
|
||||
@@ -108,10 +108,10 @@ interface EmbedNonStylesConfig {
|
||||
};
|
||||
}
|
||||
|
||||
const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => {
|
||||
const setEmbedStyles = (stylesConfig: EmbedStyles) => {
|
||||
embedStore.styles = stylesConfig;
|
||||
for (const [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) {
|
||||
(setEmbedStyle as any)((styles: any) => {
|
||||
setEmbedStyle((styles) => {
|
||||
return {
|
||||
...styles,
|
||||
...stylesConfig,
|
||||
@@ -120,15 +120,48 @@ const setEmbedStyles = (stylesConfig: UiConfig["styles"]) => {
|
||||
}
|
||||
};
|
||||
|
||||
const registerNewSetter = (elementName: keyof EmbedStyles | keyof EmbedNonStylesConfig, setStyles: any) => {
|
||||
embedStore.reactStylesStateSetters[elementName] = setStyles;
|
||||
// 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
|
||||
setStyles(embedStore.styles);
|
||||
const setEmbedNonStyles = (stylesConfig: EmbedNonStylesConfig) => {
|
||||
embedStore.nonStyles = stylesConfig;
|
||||
for (const [, setEmbedStyle] of Object.entries(embedStore.reactStylesStateSetters)) {
|
||||
setEmbedStyle((styles) => {
|
||||
return {
|
||||
...styles,
|
||||
...stylesConfig,
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const removeFromEmbedStylesSetterMap = (elementName: keyof EmbedStyles | keyof EmbedNonStylesConfig) => {
|
||||
delete embedStore.reactStylesStateSetters[elementName];
|
||||
const registerNewSetter = (
|
||||
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) {
|
||||
@@ -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
|
||||
export const useEmbedStyles = (elementName: keyof EmbedStyles) => {
|
||||
const [styles, setStyles] = useState({} as EmbedStyles);
|
||||
const [, setStyles] = useState<EmbedStyles>({});
|
||||
|
||||
useEffect(() => {
|
||||
registerNewSetter(elementName, setStyles);
|
||||
// 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.
|
||||
return () => {
|
||||
// Once the component is unmounted, we can remove that state setter.
|
||||
removeFromEmbedStylesSetterMap(elementName);
|
||||
};
|
||||
return registerNewSetter({ elementName, setState: setStyles, styles: true });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const styles = embedStore.styles || {};
|
||||
// Always read the data from global embedStore so that even across components, the same data is used.
|
||||
return styles[elementName] || {};
|
||||
};
|
||||
|
||||
export const useEmbedNonStylesConfig = (elementName: keyof EmbedNonStylesConfig) => {
|
||||
const [styles, setStyles] = useState({} as EmbedNonStylesConfig);
|
||||
const [, setNonStyles] = useState({} as EmbedNonStylesConfig);
|
||||
|
||||
useEffect(() => {
|
||||
registerNewSetter(elementName, setStyles);
|
||||
// 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.
|
||||
return () => {
|
||||
// Once the component is unmounted, we can remove that state setter.
|
||||
removeFromEmbedStylesSetterMap(elementName);
|
||||
};
|
||||
return registerNewSetter({ elementName, setState: setNonStyles, styles: false });
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
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 = () => {
|
||||
@@ -251,8 +279,8 @@ function unhideBody() {
|
||||
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
|
||||
export const methods = {
|
||||
// It is a map of methods that can be called by parent using doInIframe({method: "methodName", arg: "argument"})
|
||||
const methods = {
|
||||
ui: function style(uiConfig: UiConfig) {
|
||||
// TODO: Create automatic logger for all methods. Useful for debugging.
|
||||
log("Method: ui called", uiConfig);
|
||||
@@ -277,8 +305,10 @@ export const methods = {
|
||||
}
|
||||
|
||||
setEmbedStyles(stylesConfig || {});
|
||||
setEmbedNonStyles(stylesConfig || {});
|
||||
},
|
||||
parentKnowsIframeReady: () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
parentKnowsIframeReady: (_unused: unknown) => {
|
||||
log("Method: `parentKnowsIframeReady` called");
|
||||
runAsap(function tryInformingLinkReady() {
|
||||
// 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(
|
||||
{
|
||||
originator: "CAL",
|
||||
@@ -344,13 +381,15 @@ function keepParentInformedAboutDimensionChanges() {
|
||||
// Use, .height as that gives more accurate value in floating point. Also, do a ceil on the total sum so that whatever happens there is enough iframe size to avoid scroll.
|
||||
const contentHeight = Math.ceil(
|
||||
parseFloat(mainElementStyles.height) +
|
||||
parseFloat(mainElementStyles.marginTop) +
|
||||
parseFloat(mainElementStyles.marginBottom));
|
||||
parseFloat(mainElementStyles.marginTop) +
|
||||
parseFloat(mainElementStyles.marginBottom)
|
||||
);
|
||||
const contentWidth = Math.ceil(
|
||||
parseFloat(mainElementStyles.width) +
|
||||
parseFloat(mainElementStyles.marginLeft) +
|
||||
parseFloat(mainElementStyles.marginRight));
|
||||
|
||||
parseFloat(mainElementStyles.marginLeft) +
|
||||
parseFloat(mainElementStyles.marginRight)
|
||||
);
|
||||
|
||||
// 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.
|
||||
// On subsequent renders, consider html height as the height of the iframe. If we don't do this, then if iframe get's bigger in height, it would never shrink
|
||||
@@ -394,35 +433,18 @@ if (isBrowser) {
|
||||
|
||||
sdkActionManager?.on("*", (e) => {
|
||||
const detail = e.detail;
|
||||
//console.log(detail.fullType, detail.type, detail.data);
|
||||
log(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) => {
|
||||
const data: Record<string, any> = e.data;
|
||||
const data: Message = e.data;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
const method: keyof typeof methods = data.method;
|
||||
const method: keyof typeof interfaceWithParent = data.method;
|
||||
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 { Inline } from "./Inline/inline";
|
||||
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 type { EventData, EventDataMap } 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.
|
||||
// This is happening because we are not able to generate an App and a lib using single Vite Config.
|
||||
const WEBAPP_URL =
|
||||
@@ -18,10 +27,20 @@ customElements.define("cal-inline", Inline);
|
||||
declare module "*.css";
|
||||
type Namespace = string;
|
||||
type Config = {
|
||||
origin: string;
|
||||
calOrigin: string;
|
||||
debug?: 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;
|
||||
if (!globalCal || !globalCal.q) {
|
||||
@@ -35,9 +54,22 @@ globalCal.fingerprint = import.meta.env.EMBED_PUBLIC_EMBED_FINGER_PRINT as strin
|
||||
globalCal.__css = allCss;
|
||||
document.head.appendChild(document.createElement("style")).innerHTML = css;
|
||||
|
||||
function log(...args: any[]) {
|
||||
function log(...args: unknown[]) {
|
||||
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
|
||||
* A very simple data validator written with intention of keeping payload size low.
|
||||
@@ -45,8 +77,8 @@ function log(...args: any[]) {
|
||||
* @param data
|
||||
* @param schema
|
||||
*/
|
||||
function validate(data: any, schema: Record<"props" | "required", any>) {
|
||||
function checkType(value: any, expectedType: any) {
|
||||
function validate(data: Record<string, unknown>, schema: ValidationSchema) {
|
||||
function checkType(value: unknown, expectedType: ValidationSchemaPropType) {
|
||||
if (typeof expectedType === "string") {
|
||||
return typeof value == expectedType;
|
||||
} 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";
|
||||
}
|
||||
|
||||
@@ -62,7 +94,7 @@ function validate(data: any, schema: Record<"props" | "required", any>) {
|
||||
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])) {
|
||||
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[];
|
||||
|
||||
type PrefillAndIframeAttrsConfig = Record<string, string | string[] | Record<string, string>> & {
|
||||
iframeAttrs?: Record<string, string> & {
|
||||
id?: string;
|
||||
};
|
||||
};
|
||||
|
||||
export class Cal {
|
||||
iframe?: HTMLIFrameElement;
|
||||
|
||||
@@ -100,35 +146,42 @@ export class Cal {
|
||||
|
||||
iframeReady!: boolean;
|
||||
|
||||
iframeDoQueue: { method: keyof typeof methods; arg: any }[] = [];
|
||||
iframeDoQueue: DoInIframeArg[] = [];
|
||||
|
||||
api: CalApi;
|
||||
|
||||
static actionsManagers: Record<Namespace, SdkActionManager>;
|
||||
|
||||
static getQueryObject(config: Record<string, string>) {
|
||||
static getQueryObject(config: PrefillAndIframeAttrsConfig) {
|
||||
config = config || {};
|
||||
return {
|
||||
...config,
|
||||
// guests is better for API but Booking Page accepts guest. So do the mapping
|
||||
guest: config.guests ?? undefined,
|
||||
};
|
||||
} as PrefillAndIframeAttrsConfig & { guest?: string | string[] };
|
||||
}
|
||||
|
||||
processInstruction(instruction: Instruction) {
|
||||
instruction = [].slice.call(instruction, 0);
|
||||
if (instruction[0] instanceof Array) {
|
||||
processInstruction(instructionAsArgs: IArguments | Instruction) {
|
||||
// The instruction is actually an array-like object(arguments). Make it an 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
|
||||
instruction.forEach((instruction) => {
|
||||
this.processInstruction(instruction);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const [method, ...args] = instruction;
|
||||
if (!this[method]) {
|
||||
if (!this.api[method]) {
|
||||
// Instead of throwing error, log and move forward in the queue
|
||||
log(`Instruction ${method} not FOUND`);
|
||||
}
|
||||
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) {
|
||||
// Instead of throwing error, log and move forward in the queue
|
||||
log(`Instruction couldn't be executed`, e);
|
||||
@@ -136,7 +189,7 @@ export class Cal {
|
||||
return instruction;
|
||||
}
|
||||
|
||||
processQueue(queue: InstructionQueue) {
|
||||
processQueue(queue: IArguments[]) {
|
||||
queue.forEach((instruction) => {
|
||||
this.processInstruction(instruction);
|
||||
});
|
||||
@@ -153,9 +206,11 @@ export class Cal {
|
||||
createIframe({
|
||||
calLink,
|
||||
queryObject = {},
|
||||
calOrigin,
|
||||
}: {
|
||||
calLink: string;
|
||||
queryObject?: Record<string, string | string[] | Record<string, string>>;
|
||||
queryObject?: PrefillAndIframeAttrsConfig & { guest?: string | string[] };
|
||||
calOrigin?: string;
|
||||
}) {
|
||||
const iframe = (this.iframe = document.createElement("iframe"));
|
||||
iframe.className = "cal-embed";
|
||||
@@ -163,7 +218,7 @@ export class Cal {
|
||||
const config = this.getConfig();
|
||||
const { iframeAttrs, ...restQueryObject } = queryObject;
|
||||
|
||||
if (iframeAttrs && typeof iframeAttrs !== "string" && !(iframeAttrs instanceof Array)) {
|
||||
if (iframeAttrs && 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")) {
|
||||
// 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`;
|
||||
@@ -203,252 +258,34 @@ export class Cal {
|
||||
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() {
|
||||
return this.__config;
|
||||
}
|
||||
|
||||
// TODO: Maintain exposed methods in a separate namespace, so that unexpected methods don't become instructions
|
||||
|
||||
/**
|
||||
* 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 }) {
|
||||
doInIframe(doInIframeArg: DoInIframeArg) {
|
||||
if (!this.iframeReady) {
|
||||
this.iframeDoQueue.push({ method, arg });
|
||||
this.iframeDoQueue.push(doInIframeArg);
|
||||
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.
|
||||
this.iframe!.contentWindow!.postMessage({ originator: "CAL", method, arg }, "*");
|
||||
if (!this.iframe) {
|
||||
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 = {
|
||||
// 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.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
|
||||
this.actionManager.on("__dimensionChanged", (e) => {
|
||||
const { data } = e.detail;
|
||||
const iframe = this.iframe!;
|
||||
const iframe = this.iframe;
|
||||
|
||||
if (!iframe) {
|
||||
// Iframe might be pre-rendering
|
||||
return;
|
||||
}
|
||||
let unit = "px";
|
||||
if (data.__unit) {
|
||||
unit = data.__unit;
|
||||
}
|
||||
const unit = "px";
|
||||
if (data.iframeHeight) {
|
||||
iframe.style.height = data.iframeHeight + unit;
|
||||
}
|
||||
|
||||
// if (data.iframeWidth) {
|
||||
// iframe.style.width = data.iframeWidth + unit;
|
||||
// }
|
||||
|
||||
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
|
||||
// This case is reproducible when viewing in ModalBox on Mobile.
|
||||
@@ -490,9 +320,9 @@ export class Cal {
|
||||
|
||||
this.actionManager.on("__iframeReady", () => {
|
||||
this.iframeReady = true;
|
||||
this.doInIframe({ method: "parentKnowsIframeReady", arg: undefined });
|
||||
this.iframeDoQueue.forEach(({ method, arg }) => {
|
||||
this.doInIframe({ method, arg });
|
||||
this.doInIframe({ method: "parentKnowsIframeReady" } as const);
|
||||
this.iframeDoQueue.forEach((doInIframeArg) => {
|
||||
this.doInIframe(doInIframeArg);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -523,34 +353,276 @@ export class Cal {
|
||||
}
|
||||
}
|
||||
|
||||
export interface GlobalCal {
|
||||
(methodName: string, arg?: any): void;
|
||||
class CalApi {
|
||||
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. */
|
||||
loaded?: boolean;
|
||||
/** Maintains a queue till the time embed.js isn't loaded */
|
||||
q?: InstructionQueue;
|
||||
q: IArguments[];
|
||||
/** If user registers multiple namespaces, those are available here */
|
||||
ns?: Record<string, GlobalCal>;
|
||||
instance?: Cal;
|
||||
__css?: 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 {
|
||||
interface Window {
|
||||
Cal?: GlobalCal;
|
||||
Cal: GlobalCal;
|
||||
}
|
||||
}
|
||||
|
||||
export interface CalWindow extends Window {
|
||||
Cal?: GlobalCal;
|
||||
Cal: GlobalCal;
|
||||
}
|
||||
|
||||
globalCal.instance = new Cal("", globalCal.q!);
|
||||
|
||||
for (const [ns, api] of Object.entries(globalCal.ns!)) {
|
||||
api.instance = new Cal(ns, api.q!);
|
||||
globalCal.instance = new Cal("", globalCal.q);
|
||||
for (const [ns, api] of Object.entries(globalCal.ns)) {
|
||||
api.instance = new Cal(ns, api.q);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -571,38 +643,45 @@ window.addEventListener("message", (e) => {
|
||||
if (!actionManager) {
|
||||
throw new Error("Unhandled Action" + parsedAction);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
actionManager.fire(parsedAction.type, detail.data);
|
||||
});
|
||||
|
||||
document.addEventListener("click", (e) => {
|
||||
const htmlElement = e.target;
|
||||
if (!(htmlElement instanceof HTMLElement)) {
|
||||
const targetEl = e.target;
|
||||
if (!(targetEl instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
const path = htmlElement.dataset.calLink;
|
||||
const path = targetEl.dataset.calLink;
|
||||
if (!path) {
|
||||
return;
|
||||
}
|
||||
const modalUniqueId = ((htmlElement as unknown as any).uniqueId =
|
||||
(htmlElement as unknown as any).uniqueId || Date.now());
|
||||
const namespace = htmlElement.dataset.calNamespace;
|
||||
const configString = htmlElement.dataset.calConfig || "";
|
||||
const modalUniqueId = (targetEl.dataset.uniqueId = targetEl.dataset.uniqueId || String(Date.now()));
|
||||
const namespace = targetEl.dataset.calNamespace;
|
||||
const configString = targetEl.dataset.calConfig || "";
|
||||
const calOrigin = targetEl.dataset.calOrigin || "";
|
||||
let config;
|
||||
try {
|
||||
config = JSON.parse(configString);
|
||||
} catch (e) {
|
||||
config = {};
|
||||
}
|
||||
let api = globalCal;
|
||||
|
||||
let api: GlobalCalWithoutNs = globalCal;
|
||||
|
||||
if (namespace) {
|
||||
api = globalCal.ns![namespace];
|
||||
api = globalCal.ns[namespace];
|
||||
}
|
||||
|
||||
if (!api) {
|
||||
throw new Error(`Namespace ${namespace} isn't defined`);
|
||||
}
|
||||
|
||||
api("modal", {
|
||||
calLink: path,
|
||||
config,
|
||||
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}`;
|
||||
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
|
||||
(function (C, A, L) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const p = function (a: any, ar: any) {
|
||||
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 ||
|
||||
function () {
|
||||
const cal = C.Cal!;
|
||||
const cal = C.Cal;
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
const ar = arguments;
|
||||
if (!cal.loaded) {
|
||||
@@ -23,13 +22,16 @@ const EMBED_LIB_URL = import.meta.env.EMBED_PUBLIC_EMBED_LIB_URL || `${WEBAPP_UR
|
||||
cal.loaded = true;
|
||||
}
|
||||
if (ar[0] === L) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const api: { (): void; q?: any[] } = function () {
|
||||
// eslint-disable-next-line prefer-rest-params
|
||||
p(api, arguments);
|
||||
};
|
||||
const namespace = ar[1];
|
||||
api!.q = api.q || [];
|
||||
typeof namespace === "string" ? (cal!.ns![namespace] = api) && p(api, ar) : p(cal, ar);
|
||||
api.q = api.q || [];
|
||||
// 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;
|
||||
}
|
||||
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");
|
||||
|
||||
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,
|
||||
});
|
||||
const searchParams = new URL(document.URL).searchParams;
|
||||
const embedType = searchParams.get("embedType");
|
||||
const calLink = searchParams.get("calLink");
|
||||
if (embedType! === "inline") {
|
||||
previewWindow.Cal!("inline", {
|
||||
if (!calLink) {
|
||||
throw new Error('Missing "calLink" query parameter');
|
||||
}
|
||||
if (embedType === "inline") {
|
||||
previewWindow.Cal("inline", {
|
||||
elementOrSelector: "#my-embed",
|
||||
calLink: calLink,
|
||||
});
|
||||
} else if (embedType === "floating-popup") {
|
||||
previewWindow.Cal!("floatingButton", {
|
||||
previewWindow.Cal("floatingButton", {
|
||||
calLink: calLink,
|
||||
attributes: {
|
||||
id: "my-floating-button",
|
||||
@@ -58,7 +64,7 @@ if (embedType! === "inline") {
|
||||
});
|
||||
} else if (embedType === "element-click") {
|
||||
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";
|
||||
document.body.appendChild(button);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
type Namespace = string;
|
||||
type CustomEventDetail = Record<string, any>;
|
||||
type CustomEventDetail = Record<string, unknown>;
|
||||
|
||||
function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
||||
const event = new window.CustomEvent(fullName, {
|
||||
@@ -9,6 +9,53 @@ function _fireEvent(fullName: string, detail: CustomEventDetail) {
|
||||
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 {
|
||||
namespace: Namespace;
|
||||
|
||||
@@ -31,7 +78,7 @@ export class SdkActionManager {
|
||||
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 detail = {
|
||||
type: name,
|
||||
@@ -46,12 +93,12 @@ export class SdkActionManager {
|
||||
_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);
|
||||
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);
|
||||
window.removeEventListener(fullName, callback as EventListener);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "@calcom/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2015",
|
||||
"module": "esnext",
|
||||
"paths": {
|
||||
"@lib/*": ["../../../apps/web/lib/*"]
|
||||
|
||||
@@ -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"));
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user