This commit is contained in:
Peer Richelsen
2023-05-22 23:45:38 +01:00
103 changed files with 818 additions and 406 deletions
-28
View File
@@ -1,28 +0,0 @@
name: TODO to Issue
on:
push:
branches:
- main
workflow_dispatch:
inputs:
MANUAL_COMMIT_REF:
description: "The SHA of the commit to get the diff for"
required: true
MANUAL_BASE_REF:
description: "By default, the commit entered above is compared to the one directly before it; to go back further, enter an earlier SHA here"
required: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
todos:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: "Add TODOs to issues"
uses: "alstr/todo-to-issue-action@v4"
env:
MANUAL_COMMIT_REF: ${{ inputs.MANUAL_COMMIT_REF }}
MANUAL_BASE_REF: ${{ inputs.MANUAL_BASE_REF }}
with:
IDENTIFIERS: '[{"name": "TODO", "labels": ["🙋🏻‍♂️help wanted"]}, {"name": "FIXME", "labels": ["🐛 bug"]}]'
+6 -1
View File
@@ -86,7 +86,12 @@ To develop locally:
yarn
```
5. Start developing and watch for code changes:
5. Set up your `.env` file:
- Use `openssl rand -base64 32` to generate a key and add it under `NEXTAUTH_SECRET` in the `.env` file.
- Use `openssl rand -base64 24` to generate a key and add it under `CALENDSO_ENCRYPTION_KEY` in the `.env` file.
6. Start developing and watch for code changes:
```sh
yarn dev
+1 -1
View File
@@ -2,7 +2,7 @@ Copyright (c) 2020-present Cal.com, Inc.
Portions of this software are licensed as follows:
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Enterprise Edition) is licensed under the license defined in "ee/LICENSE".
* All content that resides under https://github.com/calcom/cal.com/tree/main/packages/features/ee directory of this repository (Commercial License) is licensed under the license defined in "ee/LICENSE".
* All third party components incorporated into the Cal.com Software are licensed under the original license provided by the owner of the applicable component.
* Content outside of the above mentioned directories or restrictions above is available under the "AGPLv3" license as defined below.
+7 -1
View File
@@ -36,6 +36,7 @@ const Component = ({
tos,
privacy,
isProOnly,
teamsPlanRequired,
descriptionItems,
isTemplate,
dependencies,
@@ -152,6 +153,7 @@ const Component = ({
type={type}
isProOnly={isProOnly}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
@@ -192,6 +194,7 @@ const Component = ({
type={type}
isProOnly={isProOnly}
disableInstall={disableInstall}
teamsPlanRequired={teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
@@ -241,7 +244,9 @@ const Component = ({
</div>
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
<span className="text-default">
{price === 0 ? (
{teamsPlanRequired ? (
t("teams_plan_required")
) : price === 0 ? (
t("free_to_use_apps")
) : (
<>
@@ -358,6 +363,7 @@ export default function App(props: {
privacy?: string;
licenseRequired: AppType["licenseRequired"];
isProOnly: AppType["isProOnly"];
teamsPlanRequired: AppType["teamsPlanRequired"];
descriptionItems?: Array<string | { iframe: IframeHTMLAttributes<HTMLIFrameElement> }>;
isTemplate?: boolean;
disableInstall?: boolean;
@@ -18,9 +18,9 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
const { t } = useLocale();
const session = useSession();
const router = useRouter();
const isAdmin = session.data?.user.role === "ADMIN";
if (session.status === "loading") return <></>;
return (
<Shell {...rest} actions={actions?.("block")} hideHeadingOnMobile>
<div className="flex flex-col xl:flex-row">
@@ -29,8 +29,8 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
<EmptyScreen
Icon={AlertCircle}
headline={t("no_apps")}
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
description={isAdmin ? "You can enable apps in the settings" : ""}
buttonText={isAdmin ? t("apps_settings") : ""}
buttonOnClick={() => router.push("/settings/admin/apps/calendar")}
/>
) : (
@@ -72,7 +72,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
slotUtcStartDate: slot.time,
eventTypeId,
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
bookingAttendees: bookingAttendees || undefined,
bookingUid: slot.bookingUid,
});
};
@@ -29,7 +29,7 @@ import {
TableActions,
TextAreaField,
} from "@calcom/ui";
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } from "@calcom/ui/components/icon";
import { Check, Clock, MapPin, RefreshCcw, Send, Ban, X, CreditCard } from "@calcom/ui/components/icon";
import useMeQuery from "@lib/hooks/useMeQuery";
@@ -119,7 +119,7 @@ function BookingListItem(booking: BookingItemProps) {
onClick: () => {
setRejectionDialogIsOpen(true);
},
icon: Slash,
icon: Ban,
disabled: mutation.isLoading,
},
// For bookings with payment, only confirm if the booking is paid for
@@ -237,7 +237,7 @@ const BookingPage = ({
eventTypeId: eventType.id,
slotUtcStartDate: dayjs(queryDate).utc().format(),
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
bookingAttendees: currentSlotBooking ? currentSlotBooking.attendees.length : undefined,
bookingUid: currentSlotBooking?.uid,
});
}
};
@@ -322,7 +322,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
disabled={seatsLocked.disabled}
defaultValue={value || 2}
min={1}
className="w-24"
addOnSuffix={<>{t("seats")}</>}
onChange={(e) => {
onChange(Math.abs(Number(e.target.value)));
@@ -108,7 +108,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
});
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const { t } = useLocale();
const { t, i18n } = useLocale();
const formMethods = useFormContext<FormValues>();
const PERIOD_TYPES = [
@@ -149,12 +149,25 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
const periodTypeLocked = shouldLockDisableProps("periodType");
const offsetStartLockedProps = shouldLockDisableProps("offsetStart");
const optionsPeriod = [
{ value: 1, label: t("calendar_days") },
{ value: 0, label: t("business_days") },
];
// offsetStart toggle is client-side only, opened by default if offsetStart is set
const offsetStartValue = useWatch({
control: formMethods.control,
name: "offsetStart",
});
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
// Preview how the offset will affect start times
const offsetOriginalTime = new Date();
offsetOriginalTime.setHours(9, 0, 0, 0);
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
return (
<div className="space-y-8">
<div className="space-y-4 lg:space-y-8">
@@ -432,6 +445,32 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</SettingsToggle>
)}
/>
<hr className="border-subtle" />
<SettingsToggle
title={t("offset_toggle")}
description={t("offset_toggle_description")}
{...offsetStartLockedProps}
checked={offsetToggle}
onCheckedChange={(active) => {
setOffsetToggle(active);
if (!active) {
formMethods.setValue("offsetStart", 0);
}
}}>
<TextField
required
type="number"
{...offsetStartLockedProps}
label={t("offset_start")}
defaultValue={eventType.offsetStart}
{...formMethods.register("offsetStart")}
addOnSuffix={<>{t("minutes")}</>}
hint={t("offset_start_description", {
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
adjustedTime: offsetAdjustedTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
})}
/>
</SettingsToggle>
</div>
);
};
@@ -1,9 +1,8 @@
import { ArrowRightIcon } from "@heroicons/react/solid";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { List } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { AppConnectionItem } from "../components/AppConnectionItem";
import { ConnectedCalendarItem } from "../components/ConnectedCalendarItem";
@@ -85,7 +84,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
onClick={() => nextStep()}
disabled={disabledNextButton}>
{firstCalendar ? `${t("continue")}` : `${t("next_step_text")}`}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</button>
</>
);
@@ -1,9 +1,8 @@
import { ArrowRightIcon } from "@heroicons/react/solid";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { List } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { AppConnectionItem } from "../components/AppConnectionItem";
import { StepConnectionLoader } from "../components/StepConnectionLoader";
@@ -59,7 +58,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
disabled={!hasAnyInstalledVideoApps}
onClick={() => nextStep()}>
{t("next_step_text")}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</button>
</>
);
@@ -1,4 +1,3 @@
import { ArrowRightIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import { useForm } from "react-hook-form";
@@ -9,6 +8,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Form } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
interface ISetupAvailabilityProps {
nextStep: () => void;
@@ -81,7 +81,7 @@ const SetupAvailability = (props: ISetupAvailabilityProps) => {
type="submit"
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
disabled={availabilityForm.formState.isSubmitting}>
{t("next_step_text")} <ArrowRightIcon className="ml-2 h-4 w-4 self-center" />
{t("next_step_text")} <ArrowRight className="ml-2 h-4 w-4 self-center" />
</Button>
</div>
</Form>
@@ -1,4 +1,3 @@
import { ArrowRightIcon } from "@heroicons/react/solid";
import { useRouter } from "next/router";
import type { FormEvent } from "react";
import { useRef, useState } from "react";
@@ -11,6 +10,7 @@ import turndown from "@calcom/lib/turndownService";
import { trpc } from "@calcom/trpc/react";
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
import { Avatar } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
@@ -159,7 +159,7 @@ const UserProfile = (props: IUserProfileProps) => {
type="submit"
className="text-inverted mt-8 flex w-full flex-row justify-center rounded-md border border-black bg-black p-2 text-center text-sm">
{t("finish")}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>
</form>
);
@@ -1,4 +1,3 @@
import { ArrowRightIcon } from "@heroicons/react/outline";
import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -10,6 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { trpc } from "@calcom/trpc/react";
import { Button, TimezoneSelect } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
@@ -116,7 +116,7 @@ const UserSettings = (props: IUserSettingsProps) => {
className="mt-8 flex w-full flex-row justify-center"
disabled={mutation.isLoading}>
{t("next_step_text")}
<ArrowRightIcon className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>
</form>
);
@@ -1,4 +1,3 @@
import { StarIcon as StarIconSolid } from "@heroicons/react/solid";
import classNames from "classnames";
import { debounce, noop } from "lodash";
import { useRouter } from "next/router";
@@ -15,6 +14,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
import { Star as StarSolid } from "@calcom/ui/components/icon";
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
export enum UsernameChangeStatusEnum {
@@ -231,11 +231,15 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<span
className={classNames(
"mx-2 py-2",
isInputUsernamePremium ? "text-orange-400" : "",
isInputUsernamePremium ? "text-transparent" : "",
usernameIsAvailable ? "" : ""
)}>
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] h-4 w-4" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 h-4 w-4" /> : <></>}
{isInputUsernamePremium ? <StarSolid className="mt-[2px] h-4 w-4 fill-orange-400" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? (
<Check className="mt-[2px] h-4 w-4" />
) : (
<></>
)}
</span>
</div>
</div>
-6
View File
@@ -111,12 +111,6 @@ const nextConfig = {
transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
preventFullImport: true,
},
"@heroicons/react/solid": {
transform: "@heroicons/react/solid/esm/{{ member }}",
},
"@heroicons/react/outline": {
transform: "@heroicons/react/outline/esm/{{ member }}",
},
"@calcom/features/insights/components": {
transform: "@calcom/features/insights/components/{{member}}",
skipDefaultConversion: true,
+1 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.9.0",
"version": "2.9.1",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -41,7 +41,6 @@
"@daily-co/daily-js": "^0.37.0",
"@formkit/auto-animate": "^1.0.0-beta.5",
"@glidejs/glide": "^3.5.2",
"@heroicons/react": "^1.0.6",
"@hookform/error-message": "^2.0.0",
"@hookform/resolvers": "^2.9.7",
"@next-auth/prisma-adapter": "^1.0.4",
+4 -3
View File
@@ -1,4 +1,3 @@
import { BadgeCheckIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
@@ -28,7 +27,7 @@ import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
import { ArrowRight } from "@calcom/ui/components/icon";
import { Verified, ArrowRight } from "@calcom/ui/components/icon";
import type { inferSSRProps } from "@lib/types/inferSSRProps";
import type { EmbedProps } from "@lib/withEmbedSsr";
@@ -139,7 +138,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<Avatar imageSrc={user.avatar} size="xl" alt={nameOrUsername} />
<h1 className="font-cal text-emphasis mb-1 text-3xl">
{nameOrUsername}
{user.verified && <BadgeCheckIcon className=" mx-1 -mt-1 inline h-6 w-6 text-blue-500" />}
{user.verified && (
<Verified className=" mx-1 -mt-1 inline h-6 w-6 fill-blue-500 text-white dark:text-black" />
)}
</h1>
{!isBioEmpty && (
<>
+1
View File
@@ -72,6 +72,7 @@ function SingleAppPage(props: inferSSRProps<typeof getStaticProps>) {
website={data.url}
email={data.email}
licenseRequired={data.licenseRequired}
teamsPlanRequired={data.teamsPlanRequired}
isProOnly={data.isProOnly}
descriptionItems={source.data?.items as string[] | undefined}
isTemplate={data.isTemplate}
+4 -4
View File
@@ -1,4 +1,3 @@
import { CheckIcon, MailOpenIcon, ExclamationIcon } from "@heroicons/react/outline";
import { signIn } from "next-auth/react";
import Head from "next/head";
import { useRouter } from "next/router";
@@ -9,6 +8,7 @@ import z from "zod";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { trpc } from "@calcom/trpc/react";
import { Button, showToast } from "@calcom/ui";
import { Check, MailOpen, AlertTriangle } from "@calcom/ui/components/icon";
import Loader from "@components/Loader";
import PageWrapper from "@components/PageWrapper";
@@ -118,11 +118,11 @@ export default function Verify() {
<div className="m-10 flex max-w-2xl flex-col items-start border border-white p-12 text-left">
<div className="rounded-full border border-white p-3">
{hasPaymentFailed ? (
<ExclamationIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
<AlertTriangle className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
) : sessionId ? (
<CheckIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
<Check className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
) : (
<MailOpenIcon className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
<MailOpen className="text-inverted h-12 w-12 flex-shrink-0 p-0.5 font-extralight" />
)}
</div>
<h3 className="font-cal my-6 text-3xl font-normal">
@@ -51,6 +51,7 @@ export type FormValues = {
eventName: string;
slug: string;
length: number;
offsetStart: number;
description: string;
disableGuests: boolean;
requiresConfirmation: boolean;
@@ -211,6 +212,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
bookingLimits: eventType.bookingLimits || undefined,
durationLimits: eventType.durationLimits || undefined,
length: eventType.length,
offsetStart: eventType.offsetStart,
hidden: eventType.hidden,
periodDates: {
startDate: periodDates.startDate,
@@ -255,6 +257,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
)
.optional(),
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
offsetStart: z.union([z.string().transform((val) => +val), z.number()]).optional(),
bookingFields: eventTypeBookingFields,
})
// TODO: Add schema for other fields later.
+17 -3
View File
@@ -14,11 +14,22 @@ import PageWrapper from "@components/PageWrapper";
type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away }: PageProps) {
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
return (
<main className="flex h-full min-h-[100dvh] items-center justify-center">
<BookerSeo username={user} eventSlug={slug} rescheduleUid={booking?.uid} />
<Booker username={user} eventSlug={slug} rescheduleBooking={booking} isAway={away} />
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
/>
</main>
);
}
@@ -64,6 +75,7 @@ async function getDynamicGroupPageProps(context: GetServerSidePropsContext) {
slug,
away: false,
trpcState: ssr.dehydrate(),
isBrandingHidden: false,
},
};
}
@@ -80,6 +92,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
},
select: {
away: true,
hideBranding: true,
},
});
@@ -103,6 +116,7 @@ async function getUserPageProps(context: GetServerSidePropsContext) {
user: username,
slug,
trpcState: ssr.dehydrate(),
isBrandingHidden: user?.hideBranding,
},
};
}
@@ -13,11 +13,22 @@ import PageWrapper from "@components/PageWrapper";
type PageProps = inferSSRProps<typeof getServerSideProps>;
export default function Type({ slug, user, booking, away }: PageProps) {
export default function Type({ slug, user, booking, away, isBrandingHidden }: PageProps) {
return (
<main className="flex h-full min-h-[100dvh] items-center justify-center">
<BookerSeo username={user} eventSlug={slug} rescheduleUid={booking?.uid} />
<Booker username={user} eventSlug={slug} rescheduleBooking={booking} isAway={away} />
<BookerSeo
username={user}
eventSlug={slug}
rescheduleUid={booking?.uid}
hideBranding={isBrandingHidden}
/>
<Booker
username={user}
eventSlug={slug}
rescheduleBooking={booking}
isAway={away}
hideBranding={isBrandingHidden}
/>
</main>
);
}
@@ -41,6 +52,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
},
select: {
id: true,
hideBranding: true,
},
});
@@ -62,6 +74,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
user: teamSlug,
slug: meetingSlug,
trpcState: ssr.dehydrate(),
isBrandingHidden: team?.hideBranding,
},
};
};
@@ -50,6 +50,7 @@ const ProfileImpersonationView = () => {
setValue,
reset,
getValues,
watch,
} = formMethods;
const isDisabled = isSubmitting || !isDirty;
@@ -63,11 +64,11 @@ const ProfileImpersonationView = () => {
}}>
<div className="flex space-x-3">
<Switch
defaultChecked={!user?.disableImpersonation}
onCheckedChange={(e) => {
setValue("disableImpersonation", !e, { shouldDirty: true });
}}
fitToHeight={true}
checked={!watch("disableImpersonation")}
/>
<div className="flex flex-col">
<Skeleton as={Label} className="text-emphasis text-sm font-semibold leading-none">
Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 B

After

Width:  |  Height:  |  Size: 743 B

+1 -3
View File
@@ -1,3 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke="white" stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-ban"><circle cx="12" cy="12" r="10"></circle><line x1="4.93" x2="19.07" y1="4.93" y2="19.07"></line></svg>

Before

Width:  |  Height:  |  Size: 309 B

After

Width:  |  Height:  |  Size: 308 B

@@ -1572,7 +1572,7 @@
"sender_name": "اسم المرسل",
"already_invited": "تمت دعوة هذا الحاضر بالفعل",
"no_recordings_found": "لم يتم العثور على تسجيلات",
"new_workflow_subtitle": "سير عمل جديد من أجل...",
"new_workflow_subtitle": "سير عمل جديد لـ...",
"reporting": "تقديم التقارير",
"reporting_feature": "عرض جميع البيانات الواردة وتنزيلها كملف CSV",
"teams_plan_required": "خطة Teams مطلوبة",
@@ -756,6 +756,10 @@
"new_event_type_to_book_description": "Create a new event type for people to book times with.",
"length": "Length",
"minimum_booking_notice": "Minimum Notice",
"offset_toggle": "Offset start times",
"offset_toggle_description": "Offset timeslots shown to bookers by a specified number of minutes",
"offset_start": "Offset by",
"offset_start_description": "e.g. this will show time slots to your bookers at {{ adjustedTime }} instead of {{ originalTime }}",
"slot_interval": "Time-slot intervals",
"slot_interval_default": "Use event length (default)",
"delete_event_type": "Delete event type?",
+68 -4
View File
@@ -151,10 +151,6 @@ const TestData = {
},
};
const ctx = {
prisma: prismaMock,
};
type App = {
slug: string;
dirName: string;
@@ -186,6 +182,7 @@ type InputEventType = {
id: number;
title?: string;
length?: number;
offsetStart?: number;
slotInterval?: number;
minimumBookingNotice?: number;
users?: { id: number }[];
@@ -711,6 +708,72 @@ describe("getSchedule", () => {
);
});
test("Start times are offset (offsetStart)", async () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
const scenarioData = {
eventTypes: [
{
id: 1,
length: 25,
offsetStart: 5,
users: [
{
id: 101,
},
],
},
],
users: [
{
...TestData.users.example,
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
},
],
hosts: [],
apps: [TestData.apps.googleCalendar],
};
createBookingScenario(scenarioData);
const schedule = await getSchedule({
eventTypeId: 1,
eventTypeSlug: "",
startTime: `${plus1DateString}T18:30:00.000Z`,
endTime: `${plus2DateString}T18:29:59.999Z`,
timeZone: Timezones["+5:30"],
});
expect(schedule).toHaveTimeSlots(
[
`04:05:00.000Z`,
`04:35:00.000Z`,
`05:05:00.000Z`,
`05:35:00.000Z`,
`06:05:00.000Z`,
`06:35:00.000Z`,
`07:05:00.000Z`,
`07:35:00.000Z`,
`08:05:00.000Z`,
`08:35:00.000Z`,
`09:05:00.000Z`,
`09:35:00.000Z`,
`10:05:00.000Z`,
`10:35:00.000Z`,
`11:05:00.000Z`,
`11:35:00.000Z`,
`12:05:00.000Z`,
],
{
dateString: plus2DateString,
}
);
});
test("Check for Date overrides", async () => {
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 });
@@ -1132,6 +1195,7 @@ function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser[]) {
seatsPerTimeSlot: null,
metadata: {},
minimumBookingNotice: 0,
offsetStart: 0,
};
const foundEvents: Record<number, boolean> = {};
const eventTypesWithUsers = eventTypes.map((eventType) => {
+27
View File
@@ -24,6 +24,7 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(24);
@@ -46,6 +47,7 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(12);
@@ -66,6 +68,7 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(0);
@@ -87,6 +90,7 @@ describe("Tests the slot logic", () => {
minimumBookingNotice: 0,
workingHours,
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(0);
@@ -108,6 +112,7 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 60,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(11);
@@ -129,8 +134,30 @@ describe("Tests the slot logic", () => {
},
],
eventLength: 20,
offsetStart: 0,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(71);
});
it("can fit 48 25 minute slots with a 5 minute offset for an empty day", async () => {
expect(
getSlots({
inviteeDate: dayjs.utc().add(1, "day"),
frequency: 25,
minimumBookingNotice: 0,
workingHours: [
{
userId: 1,
days: Array.from(Array(7).keys()),
startTime: MINUTES_DAY_START,
endTime: MINUTES_DAY_END,
},
],
eventLength: 25,
offsetStart: 5,
organizerTimeZone: "America/Toronto",
})
).toHaveLength(48);
});
});
+1 -1
View File
@@ -97,7 +97,7 @@
"dependencies": {
"city-timezones": "^1.2.1",
"eslint": "^8.34.0",
"lucide-react": "^0.130.0",
"lucide-react": "^0.171.0",
"turbo": "^1.8.3"
},
"resolutions": {
@@ -47,6 +47,7 @@ export default function OmniInstallAppButton({
<InstallAppButton
type={app.type}
isProOnly={app.isProOnly}
teamsPlanRequired={app.teamsPlanRequired}
wrapperClassName={classNames("[@media(max-width:260px)]:w-full", className)}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
@@ -11,6 +11,7 @@ import closecom_config_json from "./closecom/config.json";
import cron_config_json from "./cron/config.json";
import { metadata as dailyvideo__metadata_ts } from "./dailyvideo/_metadata";
import discord_config_json from "./discord/config.json";
import eightxeight_config_json from "./eightxeight/config.json";
import { metadata as exchange2013calendar__metadata_ts } from "./exchange2013calendar/_metadata";
import { metadata as exchange2016calendar__metadata_ts } from "./exchange2016calendar/_metadata";
import exchangecalendar_config_json from "./exchangecalendar/config.json";
@@ -75,6 +76,7 @@ export const appStoreMetadata = {
cron: cron_config_json,
dailyvideo: dailyvideo__metadata_ts,
discord: discord_config_json,
eightxeight: eightxeight_config_json,
exchange2013calendar: exchange2013calendar__metadata_ts,
exchange2016calendar: exchange2016calendar__metadata_ts,
exchangecalendar: exchangecalendar_config_json,
@@ -11,6 +11,7 @@ export const apiHandlers = {
closecom: import("./closecom/api"),
cron: import("./cron/api"),
discord: import("./discord/api"),
eightxeight: import("./eightxeight/api"),
exchange2013calendar: import("./exchange2013calendar/api"),
exchange2016calendar: import("./exchange2016calendar/api"),
exchangecalendar: import("./exchangecalendar/api"),
+14 -3
View File
@@ -7,6 +7,7 @@ import classNames from "@calcom/lib/classNames";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { CAL_URL } from "@calcom/lib/constants";
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { RouterOutputs } from "@calcom/trpc/react";
@@ -49,14 +50,17 @@ export const InstallAppButtonWithoutPlanCheck = (
export const InstallAppButton = (
props: {
isProOnly?: App["isProOnly"];
teamsPlanRequired?: App["teamsPlanRequired"];
type: App["type"];
wrapperClassName?: string;
disableInstall?: boolean;
} & InstallAppButtonProps
) => {
const { isLoading, data: user } = trpc.viewer.me.useQuery();
const { isLoading: isUserLoading, data: user } = trpc.viewer.me.useQuery();
const router = useRouter();
const proProtectionElementRef = useRef<HTMLDivElement | null>(null);
const { isLoading: isTeamPlanStatusLoading, hasTeamPlan } = useHasTeamPlan();
useEffect(() => {
const el = proProtectionElementRef.current;
if (!el) {
@@ -72,12 +76,19 @@ export const InstallAppButton = (
e.stopPropagation();
return;
}
if (props.teamsPlanRequired && !hasTeamPlan) {
// TODO: I think we should show the UpgradeTip in a Dialog here. This would solve the problem of no way to go back to the App page from the UpgradeTip page(except browser's back button)
router.push(props.teamsPlanRequired.upgradeUrl);
e.stopPropagation();
return;
}
},
true
);
}, [isLoading, user, router, props.isProOnly]);
}, [isUserLoading, user, router, props.isProOnly, hasTeamPlan, props.teamsPlanRequired]);
if (isLoading) {
if (isUserLoading || isTeamPlanStatusLoading) {
return null;
}
@@ -0,0 +1,12 @@
---
items:
- 1.jpeg
- 2.jpeg
---
{DESCRIPTION}
- High definition video conferencing on any device for up to 500 participants
- Rich content sharing and collaboration
- Enterprise grade reliability and availability for a business of five or 50,000
- 100% secured and encrypted
- Copy your room link and start scheduling calls in 8x8
+16
View File
@@ -0,0 +1,16 @@
import { createDefaultInstallation } from "@calcom/app-store/_utils/installation";
import type { AppDeclarativeHandler } from "@calcom/types/AppHandler";
import appConfig from "../config.json";
const handler: AppDeclarativeHandler = {
appType: appConfig.type,
variant: appConfig.variant,
slug: appConfig.slug,
supportsMultipleInstalls: false,
handlerType: "add",
createCredential: ({ appType, user, slug }) =>
createDefaultInstallation({ appType, userId: user.id, slug, key: {} }),
};
export default handler;
@@ -0,0 +1 @@
export { default as add } from "./add";
@@ -0,0 +1,28 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "8x8",
"slug": "eightxeight",
"type": "eightxeight_video",
"logo": "icon.svg",
"url": "https://cal.com/apps/eightxeight",
"variant": "conferencing",
"categories": [
"video"
],
"publisher": "shivamklr",
"email": "shivamkalra98@gmail.com",
"appData": {
"location": {
"type": "integrations:{SLUG}_video",
"label": "{TITLE}",
"linkType": "static",
"organizerInputPlaceholder": "https://8x8.vc/company",
"urlRegExp": "^(http|https)://(www\\.)?8x8.vc/[a-zA-Z0-9]*"
}
},
"description": "The best video conferencing solution for businesses of any size. Fully secure, reliable, packed with features and ridiculously simple to use.\r\r",
"isTemplate": false,
"__createdUsingCli": true,
"__template": "event-type-location-video-static",
"dirName": "eightxeight"
}
+1
View File
@@ -0,0 +1 @@
export * as api from "./api";
@@ -0,0 +1,14 @@
{
"$schema": "https://json.schemastore.org/package.json",
"private": true,
"name": "@calcom/eightxeight",
"version": "0.0.0",
"main": "./index.ts",
"dependencies": {
"@calcom/lib": "*"
},
"devDependencies": {
"@calcom/types": "*"
},
"description": "The best video conferencing solution for businesses of any size. Fully secure, reliable, packed with features and ridiculously simple to use.\r\r"
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.0.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 288 288" style="enable-background:new 0 0 288 288;" xml:space="preserve">
<style type="text/css">
.st0{fill:#CB2233;}
.st1{fill:#FFFFFF;}
</style>
<rect x="-0.2" y="0.5" class="st0" width="288" height="288"/>
<path class="st1" d="M172.7,132.1h-21.9l-5,9.6c-0.5,1.1-1.1,2.2-1.8,3.2h-0.2c0,0-1-1.7-1.8-3.2l-5-9.6h-21.9l17.2,26.5l-17.1,26.5
h21.5l5.8-11.1c0.6-1,1.4-3,1.4-3h0.2c0,0,0.8,2,1.4,3l5.9,11.1h21.5l-17.1-26.5L172.7,132.1L172.7,132.1z"/>
<path class="st1" d="M75.9,171.7c-5.9,0.1-10.7-4.5-10.9-10.4c0,0,0-0.1,0-0.1c0.1-4.1,1.9-8.1,4.8-11c8.8,4,16.9,7.1,16.9,12.2
C86.7,168.4,82.6,171.7,75.9,171.7L75.9,171.7z M76.5,117.3c5.9,0,9.5,3.3,9.5,8.3c-0.1,3.9-1.2,7.7-3.2,11.1
c-7.9-3.4-14.5-6.4-14.5-12.4C68.3,120.4,70.8,117.3,76.5,117.3z M98.7,144.5c0.9-1.1,8.8-10.9,8.8-20.6c0-16.8-13.8-26.1-30.5-26.1
c-21.1,0-30.3,12.9-30.3,26c0,7.7,3.4,13.1,8.2,17.3c-2.8,2.1-12.5,10.4-12.5,22c0,14.4,11.5,28,33.4,28c22.3,0,33.4-13.8,33.4-27.5
C109.3,154.5,104.8,148.7,98.7,144.5L98.7,144.5z"/>
<path class="st1" d="M211.7,171.7c-5.9,0.1-10.7-4.5-10.8-10.4c0,0,0-0.1,0-0.1c0.1-4.1,1.8-8.1,4.8-11c8.8,4,16.9,7.1,16.9,12.2
C222.5,168.4,218.4,171.7,211.7,171.7L211.7,171.7z M212.3,117.3c5.9,0,9.5,3.3,9.5,8.3c-0.1,3.9-1.2,7.7-3.2,11.1
c-7.9-3.4-14.5-6.4-14.5-12.4C204.1,120.4,206.7,117.3,212.3,117.3z M234.5,144.5c0.9-1.1,8.8-10.9,8.8-20.6
c0-16.8-13.8-26.1-30.6-26.1c-21.1,0-30.3,12.9-30.3,26c0,7.7,3.4,13.1,8.2,17.3c-2.8,2.1-12.5,10.4-12.5,22c0,14.4,11.5,28,33.4,28
c22.3,0,33.4-13.8,33.4-27.5C245.1,154.5,240.6,148.7,234.5,144.5L234.5,144.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

@@ -1,10 +1,10 @@
import { LinkIcon, SearchIcon } from "@heroicons/react/outline";
import type { Dispatch, SetStateAction } from "react";
import { useState } from "react";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Alert, Button, Dialog, DialogClose, DialogContent, DialogFooter, Input } from "@calcom/ui";
import { Link, Search } from "@calcom/ui/components/icon";
interface ISearchDialog {
isOpenDialog: boolean;
@@ -118,8 +118,8 @@ export const SearchDialog = (props: ISearchDialog) => {
</h3>
<p className="text-subtle mb-3 text-sm font-light">{t("find_gif_spice_confirmation")}</p>
<div className="border-emphasis flex items-center border-b border-solid">
{renderTab(SearchIcon, t("search_giphy"), MODE_SEARCH)}
{renderTab(LinkIcon, t("add_link_from_giphy"), MODE_URL)}
{renderTab(Search, t("search_giphy"), MODE_SEARCH)}
{renderTab(Link, t("add_link_from_giphy"), MODE_URL)}
</div>
<form
className="flex w-full justify-center space-x-2 space-y-2 rtl:space-x-reverse"
@@ -1,8 +1,8 @@
import { PencilAltIcon, PlusIcon, XIcon } from "@heroicons/react/solid";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { Edit, Plus, X } from "@calcom/ui/components/icon";
import { SearchDialog } from "./SearchDialog";
@@ -24,11 +24,11 @@ export default function SelectGifInput(props: ISelectGifInput) {
)}
<div className="flex">
{selectedGif ? (
<Button color="minimal" type="button" StartIcon={PencilAltIcon} onClick={() => setShowDialog(true)}>
<Button color="minimal" type="button" StartIcon={Edit} onClick={() => setShowDialog(true)}>
Change
</Button>
) : (
<Button color="minimal" type="button" StartIcon={PlusIcon} onClick={() => setShowDialog(true)}>
<Button color="minimal" type="button" StartIcon={Plus} onClick={() => setShowDialog(true)}>
Add from Giphy
</Button>
)}
@@ -37,7 +37,7 @@ export default function SelectGifInput(props: ISelectGifInput) {
<Button
color="destructive"
type="button"
StartIcon={XIcon}
StartIcon={X}
onClick={() => {
setSelectedGif("");
props.onChange("");
+1 -1
View File
@@ -20,7 +20,7 @@ export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
key: {},
});
return { url: getInstalledAppPath({ variant: "other", slug: "ping" }) };
return { url: getInstalledAppPath({ variant: appConfig.variant, slug: "ping" }) };
}
export default defaultResponder(getHandler);
+1 -1
View File
@@ -6,4 +6,4 @@ items:
- 4.png
---
Quickly share your Cal.com meeting links with Raycast. Requires Raycast.com to be installed. You can create an API token in your Developer Cal.com Settings.
Quickly share your Cal.com meeting links with Raycast. Requires Raycast.com to be installed and the Cal.com from the Raycast Store needs to be installed: https://www.raycast.com/cal/. You can create an API token in your Developer Cal.com Settings, and then you can add the API Key by opening up the Cal.com App in Raycast.
@@ -11,6 +11,9 @@
"publisher": "Cal.com",
"email": "help@cal.com",
"licenseRequired": true,
"teamsPlanRequired": {
"upgradeUrl": "/apps/routing-forms/forms"
},
"description": "It would allow a booker to connect with the right person or choose the right event, faster. It would work by taking inputs from the booker and using that data to route to the correct booker/event as configured by Cal user",
"__createdUsingCli": true
}
@@ -1,4 +1,3 @@
import { ClockIcon } from "@heroicons/react/outline";
import { useMutation } from "@tanstack/react-query";
import type { Dispatch, SetStateAction } from "react";
import { useState } from "react";
@@ -8,6 +7,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import logger from "@calcom/lib/logger";
import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, showToast } from "@calcom/ui";
import { Clock } from "@calcom/ui/components/icon";
interface IConfirmDialogWipe {
isOpenDialog: boolean;
@@ -82,7 +82,7 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
<DialogContent onOpenAutoFocus={(e) => e.preventDefault()}>
<div className="flex flex-row space-x-3">
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
<ClockIcon className="m-auto h-6 w-6" />
<Clock className="m-auto h-5 w-5" />
</div>
<div className="pt-1">
<DialogHeader title="Wipe My Calendar" />
+5 -5
View File
@@ -455,9 +455,9 @@ export default class EventManager {
return [];
}
const { uid: bookingRefUid, externalCalendarId: bookingExternalCalendarId } = calendarReference;
if (!bookingExternalCalendarId) {
throw new Error("externalCalendarId");
let calenderExternalId: string | null = null;
if (bookingExternalCalendarId) {
calenderExternalId = bookingExternalCalendarId;
}
let result = [];
@@ -465,13 +465,13 @@ export default class EventManager {
credential = this.calendarCredentials.filter(
(credential) => credential.id === calendarReference?.credentialId
)[0];
result.push(updateEvent(credential, event, bookingRefUid, bookingExternalCalendarId));
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
} else {
const credentials = this.calendarCredentials.filter(
(credential) => credential.type === calendarReference?.type
);
for (const credential of credentials) {
result.push(updateEvent(credential, event, bookingRefUid, bookingExternalCalendarId));
result.push(updateEvent(credential, event, bookingRefUid, calenderExternalId));
}
}
@@ -65,11 +65,10 @@ export const CallToAction = (props: {
marginLeft: 0,
}}
iconName={startIconName}
secondary={secondary}
/>
)}
{label}
{endIconName && <CallToActionIcon iconName={endIconName} secondary={secondary} />}
{endIconName && <CallToActionIcon iconName={endIconName} />}
</a>
</p>
);
@@ -2,15 +2,7 @@ import React from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
export const CallToActionIcon = ({
secondary,
iconName,
style,
}: {
secondary?: boolean;
iconName: string;
style?: React.CSSProperties;
}) => (
export const CallToActionIcon = ({ iconName, style }: { iconName: string; style?: React.CSSProperties }) => (
<img
src={`${WEBAPP_URL}/emails/${iconName}.png`}
srcSet={`${WEBAPP_URL}/emails/${iconName}.svg`}
@@ -19,7 +11,6 @@ export const CallToActionIcon = ({
height: "1rem",
width: "1rem",
marginLeft: "0.5rem",
...(secondary && { filter: "brightness(50%)" }),
...style,
}}
alt=""
@@ -112,7 +112,7 @@ ${this.t(
)}
${this.t(subtitle)}
${getRichDescription(this.calEvent)}
${getRichDescription(this.calEvent, this.t)}
`.trim();
}
@@ -91,7 +91,7 @@ ${this.t("request_reschedule_subtitle", {
})},
${this.getWhen()}
${this.t("need_to_reschedule_or_cancel")}
${getManageLink(this.calEvent)}
${getManageLink(this.calEvent, this.t)}
`.replace(/(<([^>]+)>)/gi, "");
}
}
+12 -1
View File
@@ -55,6 +55,7 @@ if (only === "all" || only === "ns:second") {
iframeAttrs: {
id: "cal-booking-place-second-iframe",
},
theme: "auto",
},
},
]);
@@ -333,6 +334,16 @@ Cal("init", "routingFormDark", {
if (only === "all" || only == "ns:floatingButton") {
Cal.ns.floatingButton("floatingButton", {
calLink: "pro",
calLink: "pro/30min",
config: {
iframeAttrs: {
id: "floatingtest",
},
name: "John",
email: "johndoe@gmail.com",
notes: "Test Meeting",
guests: ["janedoe@example.com", "test@example.com"],
theme: "dark",
},
});
}
@@ -4,6 +4,7 @@ type ModalTargetDatasetProps = {
calLink: string;
calNamespace: string;
calOrigin: string;
calConfig: string;
};
type CamelCase<T extends string> = T extends `${infer U}${infer V}` ? `${Uppercase<U>}${V}` : T;
@@ -8,10 +8,10 @@ import type { Message } from "./embed";
import { sdkActionManager } from "./sdk-event";
type Theme = "dark" | "light";
export type EmbedThemeConfig = Theme | "auto";
export type UiConfig = {
hideEventTypeDetails?: boolean;
theme?: Theme | "auto";
theme?: EmbedThemeConfig;
styles?: EmbedStyles & EmbedNonStylesConfig;
//TODO: Extract from tailwind the list of all custom variables and support them in auto-completion as well as runtime validation. Followup with listing all variables in Embed Snippet Generator UI.
cssVarsPerTheme?: Record<Theme, Record<string, string>>;
@@ -31,7 +31,7 @@ const embedStore = {
reactNonStylesStateSetters: {} as Record<keyof EmbedNonStylesConfig, setNonStylesConfig>,
parentInformedAboutContentHeight: false,
windowLoadEventFired: false,
setTheme: undefined as ((arg0: string) => void) | undefined,
setTheme: undefined as ((arg0: EmbedThemeConfig) => void) | undefined,
theme: undefined as UiConfig["theme"],
uiConfig: undefined as Omit<UiConfig, "styles" | "theme"> | undefined,
setUiConfig: undefined as ((arg0: UiConfig) => void) | undefined,
@@ -170,14 +170,14 @@ function isValidNamespace(ns: string | null | undefined) {
export const useEmbedTheme = () => {
const router = useRouter();
const [theme, setTheme] = useState(embedStore.theme || (router.query.theme as string));
const [theme, setTheme] = useState(embedStore.theme || (router.query.theme as typeof embedStore.theme));
useEffect(() => {
router.events.on("routeChangeComplete", () => {
sdkActionManager?.fire("__routeChanged", {});
});
}, [router.events]);
embedStore.setTheme = setTheme;
return theme === "auto" ? null : theme;
return theme;
};
export const useEmbedUiConfig = () => {
@@ -434,7 +434,7 @@ if (isBrowser) {
// Exposes certain global variables/fns that are used by the app to get interface with the embed.
embedInit();
const url = new URL(document.URL);
embedStore.theme = (window?.getEmbedTheme?.() || "auto") as UiConfig["theme"];
embedStore.theme = window?.getEmbedTheme?.() as UiConfig["theme"];
if (url.searchParams.get("prerender") !== "true" && window?.isEmbed?.()) {
log("Initializing embed-iframe");
// HACK
+8 -1
View File
@@ -2,7 +2,7 @@
import { FloatingButton } from "./FloatingButton/FloatingButton";
import { Inline } from "./Inline/inline";
import { ModalBox } from "./ModalBox/ModalBox";
import type { InterfaceWithParent, interfaceWithParent, UiConfig } from "./embed-iframe";
import type { InterfaceWithParent, interfaceWithParent, UiConfig, EmbedThemeConfig } from "./embed-iframe";
import css from "./embed.css";
import type { EventData, EventDataMap } from "./sdk-action-manager";
import { SdkActionManager } from "./sdk-action-manager";
@@ -129,6 +129,7 @@ type PrefillAndIframeAttrsConfig = Record<string, string | string[] | Record<str
iframeAttrs?: Record<string, string> & {
id?: string;
};
theme?: EmbedThemeConfig;
};
export class Cal {
@@ -435,6 +436,7 @@ class CalApi {
buttonColor = "rgb(0, 0, 0)",
buttonTextColor = "rgb(255, 255, 255)",
calOrigin,
config,
}: {
calLink: string;
buttonText?: string;
@@ -444,6 +446,7 @@ class CalApi {
buttonColor?: string;
buttonTextColor?: string;
calOrigin?: string;
config?: PrefillAndIframeAttrsConfig;
}) {
// validate(arguments[0], {
// required: true,
@@ -466,6 +469,10 @@ class CalApi {
el.dataset.calLink = calLink;
el.dataset.calNamespace = this.cal.namespace;
el.dataset.calOrigin = calOrigin ?? "";
if (config) {
el.dataset.calConfig = JSON.stringify(config);
}
if (attributes?.id) {
el.id = attributes.id;
}
+11 -12
View File
@@ -5,10 +5,9 @@ import StickyBox from "react-sticky-box";
import { shallow } from "zustand/shallow";
import classNames from "@calcom/lib/classNames";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
import { ToggleGroup, useCalcomTheme } from "@calcom/ui";
import { ToggleGroup } from "@calcom/ui";
import { Calendar, Columns, Grid } from "@calcom/ui/components/icon";
import { AvailableTimeSlots } from "./components/AvailableTimeSlots";
@@ -23,21 +22,20 @@ import { fadeInLeft, getBookerSizeClassNames, useBookerResizeAnimation } from ".
import { useBookerStore, useInitializeBookerStore } from "./store";
import type { BookerLayout, BookerProps } from "./types";
import { useEvent } from "./utils/event";
import { useBrandColors } from "./utils/use-brand-colors";
const PoweredBy = dynamic(() => import("@calcom/ee/components/PoweredBy"));
const DatePicker = dynamic(() => import("./components/DatePicker").then((mod) => mod.DatePicker), {
ssr: false,
});
const useBrandColors = ({ brandColor, darkBrandColor }: { brandColor?: string; darkBrandColor?: string }) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
};
const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: BookerProps) => {
const BookerComponent = ({
username,
eventSlug,
month,
rescheduleBooking,
hideBranding = false,
}: BookerProps) => {
const { t } = useLocale();
const isMobile = useMediaQuery("(max-width: 768px)");
const isTablet = useMediaQuery("(max-width: 1024px)");
@@ -61,6 +59,7 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book
useBrandColors({
brandColor: event.data?.profile.brandColor,
darkBrandColor: event.data?.profile.darkBrandColor,
theme: event.data?.profile.theme,
});
useInitializeBookerStore({
@@ -209,7 +208,7 @@ const BookerComponent = ({ username, eventSlug, month, rescheduleBooking }: Book
"mt-auto mb-6 pt-6 [&_img]:h-[15px]",
layout === "small_calendar" ? "block" : "hidden"
)}>
<PoweredBy logoOnly />
{!hideBranding ? <PoweredBy logoOnly /> : null}
</m.span>
</div>
@@ -0,0 +1,26 @@
import { useEffect } from "react";
import useGetBrandingColours from "@calcom/lib/getBrandColours";
import useTheme from "@calcom/lib/hooks/useTheme";
import { useCalcomTheme } from "@calcom/ui";
export const useBrandColors = ({
brandColor,
darkBrandColor,
theme,
}: {
brandColor?: string;
darkBrandColor?: string;
theme?: string | null;
}) => {
const brandTheme = useGetBrandingColours({
lightVal: brandColor,
darkVal: darkBrandColor,
});
useCalcomTheme(brandTheme);
const { setTheme } = useTheme(theme);
useEffect(() => {
if (theme) setTheme(theme);
}, [setTheme, theme]);
};
@@ -11,7 +11,7 @@ import { useFilterQuery } from "../lib/useFilterQuery";
export const TeamsMemberFilter = () => {
const { t } = useLocale();
const session = useSession();
const { data: query, pushItemToKey, removeItemByKeyAndValue, removeByKey } = useFilterQuery();
const { data: query, pushItemToKey, removeItemByKeyAndValue, removeByKey, removeAllQueryParams } = useFilterQuery();
const { data } = trpc.viewer.teams.list.useQuery();
const [dropdownTitle, setDropdownTitle] = useState<string>(t("all_bookings_filter_label"));
@@ -38,7 +38,7 @@ export const TeamsMemberFilter = () => {
checked={!query.teamIds && !query.userIds?.includes(session.data?.user.id || 0)}
onChange={() => {
setDropdownTitle(t("all_bookings_filter_label"));
removeByKey("teamIds"); // Always clear on toggle or not toggle (seems weird but when you know the behviour it works well )
removeAllQueryParams();
}}
className="text-primary-600 focus:ring-primary-500 border-default inline-flex h-4 w-4 place-self-center justify-self-end rounded "
/>
@@ -57,7 +57,7 @@ export const TeamsMemberFilter = () => {
id="yourBookings"
type="checkbox"
disabled={session.status === "loading"}
checked={query.userIds?.includes(session.data?.user.id || 0)}
checked={!!query.userIds?.includes(session.data?.user.id || 0)}
onChange={(e) => {
setDropdownTitle(t("your_bookings_filter_label"));
if (e.target.checked) {
+3 -4
View File
@@ -1,5 +1,3 @@
import { ChevronLeftIcon, ChevronRightIcon } from "@heroicons/react/solid";
import type { Dayjs } from "@calcom/dayjs";
import dayjs from "@calcom/dayjs";
import { useEmbedStyles } from "@calcom/embed-core/embed-iframe";
@@ -8,6 +6,7 @@ import { daysInMonth, yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { weekdayNames } from "@calcom/lib/weekday";
import { Button, SkeletonText } from "@calcom/ui";
import { ChevronLeft, ChevronRight } from "@calcom/ui/components/icon";
import { ArrowRight } from "@calcom/ui/components/icon";
export type DatePickerProps = {
@@ -223,7 +222,7 @@ const DatePicker = ({
data-testid="decrementMonth"
color="minimal"
variant="icon"
StartIcon={ChevronLeftIcon}
StartIcon={ChevronLeft}
/>
<Button
className="group p-1 opacity-70 hover:opacity-100"
@@ -231,7 +230,7 @@ const DatePicker = ({
data-testid="incrementMonth"
color="minimal"
variant="icon"
StartIcon={ChevronRightIcon}
StartIcon={ChevronRight}
/>
</div>
</div>
+7 -7
View File
@@ -1,4 +1,4 @@
The Cal.com Enterprise Edition (EE) license (the “EE License”)
The Cal.com Commercial License (the “Commercial License”)
Copyright (c) 2020-present Cal.com, Inc
With regard to the Cal.com Software:
@@ -6,15 +6,15 @@ With regard to the Cal.com Software:
This software and associated documentation files (the "Software") may only be
used in production, if you (and any entity that you represent) have agreed to,
and are in compliance with, the Cal.com Subscription Terms available
at https://cal.com/terms (the “EE Terms”), or other agreements governing
at https://cal.com/terms, or other agreements governing
the use of the Software, as mutually agreed by you and Cal.com, Inc ("Cal.com"),
and otherwise have a valid Cal.com Enterprise Edition subscription ("EE Subscription")
for the correct number of hosts as defined in the EE Terms ("Hosts"). Subject to the foregoing sentence,
and otherwise have a valid Cal.com Enterprise Edition subscription ("Commercial Subscription")
for the correct number of hosts as defined in the "Commercial Terms ("Hosts"). Subject to the foregoing sentence,
you are free to modify this Software and publish patches to the Software. You agree
that Cal.com and/or its licensors (as applicable) retain all right, title and interest in
and to all such modifications and/or patches, and all such modifications and/or
patches may only be used, copied, modified, displayed, distributed, or otherwise
exploited with a valid EE Subscription for the correct number of hosts.
exploited with a valid Commercial Subscription for the correct number of hosts.
Notwithstanding the foregoing, you may copy and modify the Software for development
and testing purposes, without requiring a subscription. You agree that Cal.com and/or
its licensors (as applicable) retain all right, title and interest in and to all such
@@ -22,11 +22,11 @@ modifications. You are not granted any other rights beyond what is expressly sta
Subject to the foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
and/or sell the Software.
This EE License applies only to the part of this Software that is not distributed under
This Commercial License applies only to the part of this Software that is not distributed under
the AGPLv3 license. Any part of this Software distributed under the MIT license or which
is served client-side as an image, font, cascading stylesheet (CSS), file which produces
or is compiled, arranged, augmented, or combined into client-side JavaScript, in whole or
in part, is copyrighted under the AGPLv3 license. The full text of this EE License shall
in part, is copyrighted under the AGPLv3 license. The full text of this Commercial License shall
be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
@@ -41,6 +41,7 @@ export default function TeamAvailabilityTimes(props: Props) {
inviteeDate: props.selectedDate,
workingHours: data?.workingHours || [],
minimumBookingNotice: 0,
offsetStart: 0,
eventLength: props.frequency,
organizerTimeZone: `${data?.timeZone}`,
})
@@ -12,7 +12,7 @@ import {
DropdownItem,
DropdownMenuTrigger,
} from "@calcom/ui";
import { Slash, Check, MoreHorizontal, X } from "@calcom/ui/components/icon";
import { Ban, Check, MoreHorizontal, X } from "@calcom/ui/components/icon";
interface Props {
team: {
@@ -92,7 +92,7 @@ export default function TeamInviteListItem(props: Props) {
variant="icon"
color="secondary"
onClick={declineInvite}
StartIcon={Slash}
StartIcon={Ban}
/>
<Button
type="button"
@@ -48,7 +48,6 @@ function MembersList(props: MembersListProps) {
autoComplete="false"
onChange={(e) => setQuery(e.target.value)}
value={query}
defaultValue=""
placeholder={`${t("search")}...`}
/>
{membersList?.length && team ? (
@@ -102,10 +102,10 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
where: {
method: WorkflowMethods.EMAIL,
scheduled: false,
cancelled: false,
scheduledDate: {
lte: dayjs().add(72, "hour").toISOString(),
},
OR: [{ cancelled: false }, { cancelled: null }],
},
include: {
workflowStep: true,
@@ -36,7 +36,7 @@ export const CheckedTeamSelect = ({
}}
name={props.name}
placeholder={props.placeholder || t("select")}
isSearchable={false}
isSearchable={true}
options={options}
value={value}
isMulti
@@ -48,7 +48,7 @@ export const CheckedTeamSelect = ({
className={classNames("mt-3 rounded-md", value.length >= 1 && "border-subtle border")}
ref={animationRef}>
{value.map((option, index) => (
<li key={option.value} className={`flex py-2 px-3 ${index === value.length - 1 ? "" : "border-b"}`}>
<li key={option.value} className={`flex py-2 px-3 ${index === value.length - 1 ? "" : "border-subtle border-b"}`}>
<Avatar size="sm" imageSrc={option.avatar} alt={option.label} />
<p className="text-emphasis ms-3 my-auto text-sm">{option.label}</p>
<X
@@ -54,6 +54,7 @@ const publicEventSelect = Prisma.validator<Prisma.EventTypeSelect>()({
weekStart: true,
brandColor: true,
darkBrandColor: true,
theme: true,
},
},
},
@@ -80,6 +81,7 @@ export const getPublicEvent = async (username: string, eventSlug: string, prisma
metadata: true,
brandColor: true,
darkBrandColor: true,
theme: true,
},
});
@@ -113,6 +115,7 @@ export const getPublicEvent = async (username: string, eventSlug: string, prisma
image: `${WEBAPP_URL}/${users[0].username}/avatar.png`,
brandColor: users[0].brandColor,
darkBrandColor: users[0].darkBrandColor,
theme: null,
},
};
}
@@ -179,6 +182,7 @@ function getProfileFromEvent(event: Event) {
logo: !team ? undefined : team.logo,
brandColor: profile.brandColor,
darkBrandColor: profile.darkBrandColor,
theme: profile.theme,
};
}
@@ -12,6 +12,7 @@ import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MembershipRole, UserPermissionRole } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import useAvatarQuery from "@calcom/trpc/react/hooks/useAvatarQuery";
import type { VerticalTabItemProps } from "@calcom/ui";
import { Badge, Button, ErrorBoundary, Skeleton, useMeta, VerticalTabItem } from "@calcom/ui";
import {
@@ -105,14 +106,16 @@ const adminRequiredKeys = ["admin"];
const useTabs = () => {
const session = useSession();
const { data: user } = trpc.viewer.me.useQuery();
const { data: avatar } = useAvatarQuery();
const isAdmin = session.data?.user.role === UserPermissionRole.ADMIN;
tabs.map((tab) => {
if (tab.name === "my_account") {
tab.name = session.data?.user?.name || "my_account";
if (tab.href === "/settings/my-account") {
tab.name = user?.name || "my_account";
tab.icon = undefined;
tab.avatar = WEBAPP_URL + "/" + session.data?.user?.username + "/avatar.png";
tab.avatar = avatar?.avatar || WEBAPP_URL + "/" + session?.data?.user?.username + "/avatar.png";
}
return tab;
});
@@ -128,7 +131,7 @@ const BackButtonInSidebar = ({ name }: { name: string }) => {
return (
<Link
href="/"
className="hover:bg-subtle [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default text-emphasis group my-6 flex h-6 max-h-6 w-64 flex-row items-center rounded-md py-2 px-3 text-sm font-medium leading-4"
className="hover:bg-subtle [&[aria-current='page']]:bg-emphasis [&[aria-current='page']]:text-emphasis group-hover:text-default text-emphasis group my-6 flex h-6 max-h-6 w-full flex-row items-center rounded-md py-2 px-3 text-sm font-medium leading-4"
data-testid={`vertical-tab-${name}`}>
<ArrowLeft className="h-4 w-4 stroke-[2px] ltr:mr-[10px] rtl:ml-[10px] rtl:rotate-180 md:mt-0" />
<Skeleton title={name} as="p" className="max-w-36 min-h-4 truncate">
+5 -3
View File
@@ -24,6 +24,7 @@ import getBrandColours from "@calcom/lib/getBrandColours";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { isKeyInObject } from "@calcom/lib/isKeyInObject";
import { trpc } from "@calcom/trpc/react";
import useAvatarQuery from "@calcom/trpc/react/hooks/useAvatarQuery";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import {
@@ -246,8 +247,9 @@ export default function Shell(props: LayoutProps) {
function UserDropdown({ small }: { small?: boolean }) {
const { t } = useLocale();
const query = useMeQuery();
const user = query.data;
const { data: user } = useMeQuery();
const { data: avatar } = useAvatarQuery();
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
@@ -294,7 +296,7 @@ function UserDropdown({ small }: { small?: boolean }) {
// eslint-disable-next-line @next/next/no-img-element
<img
className="rounded-full"
src={WEBAPP_URL + "/" + user.username + "/avatar.png"}
src={avatar?.avatar || WEBAPP_URL + "/" + user.username + "/avatar.png"}
alt={user.username || "Nameless User"}
/>
}
+34 -31
View File
@@ -1,3 +1,4 @@
import type { TFunction } from "next-i18next";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
@@ -10,26 +11,26 @@ const translator = short();
// The odd indentation in this file is necessary because otherwise the leading tabs will be applied into the event description.
export const getWhat = (calEvent: CalendarEvent) => {
export const getWhat = (calEvent: CalendarEvent, t: TFunction) => {
return `
${calEvent.organizer.language.translate("what")}:
${t("what")}:
${calEvent.type}
`;
};
export const getWhen = (calEvent: CalendarEvent) => {
export const getWhen = (calEvent: CalendarEvent, t: TFunction) => {
return calEvent.seatsPerTimeSlot
? `
${calEvent.organizer.language.translate("organizer_timezone")}:
${t("organizer_timezone")}:
${calEvent.organizer.timeZone}
`
: `
${calEvent.organizer.language.translate("invitee_timezone")}:
${t("invitee_timezone")}:
${calEvent.attendees[0].timeZone}
`;
};
export const getWho = (calEvent: CalendarEvent) => {
export const getWho = (calEvent: CalendarEvent, t: TFunction) => {
let attendeesFromCalEvent = [...calEvent.attendees];
if (calEvent.seatsPerTimeSlot && !calEvent.seatsShowAttendees) {
attendeesFromCalEvent = [];
@@ -37,38 +38,38 @@ export const getWho = (calEvent: CalendarEvent) => {
const attendees = attendeesFromCalEvent
.map((attendee) => {
return `
${attendee?.name || calEvent.organizer.language.translate("guest")}
${attendee?.name || t("guest")}
${attendee.email}
`;
})
.join("");
const organizer = `
${calEvent.organizer.name} - ${calEvent.organizer.language.translate("organizer")}
${calEvent.organizer.name} - ${t("organizer")}
${calEvent.organizer.email}
`;
const teamMembers = calEvent.team?.members
? calEvent.team.members.map((member) => {
return `
${member.name} - ${calEvent.organizer.language.translate("team_member")}
${member.name} - ${t("team_member")}
${member.email}
`;
})
: [];
return `
${calEvent.organizer.language.translate("who")}:
${t("who")}:
${organizer + attendees + teamMembers.join("")}
`;
};
export const getAdditionalNotes = (calEvent: CalendarEvent) => {
export const getAdditionalNotes = (calEvent: CalendarEvent, t: TFunction) => {
if (!calEvent.additionalNotes) {
return "";
}
return `
${calEvent.organizer.language.translate("additional_notes")}:
${t("additional_notes")}:
${calEvent.additionalNotes}
`;
};
@@ -94,11 +95,11 @@ ${labelValueMap[key]}
return responsesString;
};
export const getAppsStatus = (calEvent: CalendarEvent) => {
export const getAppsStatus = (calEvent: CalendarEvent, t: TFunction) => {
if (!calEvent.appsStatus) {
return "";
}
return `\n${calEvent.organizer.language.translate("apps_status")}
return `\n${t("apps_status")}
${calEvent.appsStatus.map((app) => {
return `\n- ${app.appName} ${
app.success >= 1 ? `✅ ${app.success > 1 ? `(x${app.success})` : ""}` : ""
@@ -111,11 +112,11 @@ export const getAppsStatus = (calEvent: CalendarEvent) => {
`;
};
export const getDescription = (calEvent: CalendarEvent) => {
export const getDescription = (calEvent: CalendarEvent, t: TFunction) => {
if (!calEvent.description) {
return "";
}
return `\n${calEvent.organizer.language.translate("description")}
return `\n${t("description")}
${calEvent.description}
`;
};
@@ -153,9 +154,9 @@ const getSeatReferenceId = (calEvent: CalendarEvent): string => {
return calEvent.attendeeSeatId ? calEvent.attendeeSeatId : "";
};
export const getManageLink = (calEvent: CalendarEvent) => {
export const getManageLink = (calEvent: CalendarEvent, t: TFunction) => {
return `
${calEvent.organizer.language.translate("need_to_reschedule_or_cancel")}
${t("need_to_reschedule_or_cancel")}
${WEBAPP_URL + "/booking/" + getUid(calEvent) + "?changes=true"}
`;
};
@@ -176,27 +177,29 @@ export const getRescheduleLink = (calEvent: CalendarEvent): string => {
return `${WEBAPP_URL}/reschedule/${seatUid ? seatUid : Uid}`;
};
export const getRichDescription = (calEvent: CalendarEvent /*, attendee?: Person*/) => {
export const getRichDescription = (calEvent: CalendarEvent, t_?: TFunction /*, attendee?: Person*/) => {
const t = t_ ?? calEvent.organizer.language.translate;
return `
${getCancellationReason(calEvent)}
${getWhat(calEvent)}
${getWhen(calEvent)}
${getWho(calEvent)}
${calEvent.organizer.language.translate("where")}:
${getCancellationReason(calEvent, t)}
${getWhat(calEvent, t)}
${getWhen(calEvent, t)}
${getWho(calEvent, t)}
${t("where")}:
${getLocation(calEvent)}
${getDescription(calEvent)}
${getAdditionalNotes(calEvent)}
${getDescription(calEvent, t)}
${getAdditionalNotes(calEvent, t)}
${getUserFieldsResponses(calEvent)}
${getAppsStatus(calEvent)}
${getAppsStatus(calEvent, t)}
${
// TODO: Only the original attendee can make changes to the event
// Guests cannot
!calEvent.seatsPerTimeSlot && getManageLink(calEvent)
!calEvent.seatsPerTimeSlot && getManageLink(calEvent, t)
}
${
calEvent.paymentInfo
? `
${calEvent.organizer.language.translate("pay_now")}:
${t("pay_now")}:
${calEvent.paymentInfo.link}
`
: ""
@@ -204,10 +207,10 @@ ${calEvent.paymentInfo.link}
`.trim();
};
export const getCancellationReason = (calEvent: CalendarEvent) => {
export const getCancellationReason = (calEvent: CalendarEvent, t: TFunction) => {
if (!calEvent.cancellationReason) return "";
return `
${calEvent.organizer.language.translate("cancellation_reason")}:
${t("cancellation_reason")}:
${calEvent.cancellationReason}
`;
};
+1
View File
@@ -63,6 +63,7 @@ const commons = {
periodType: PeriodType.UNLIMITED,
periodDays: null,
slotInterval: null,
offsetStart: 0,
locations: [{ type: DailyLocationType }],
customInputs,
disableGuests: true,
+1
View File
@@ -78,6 +78,7 @@ export default async function getEventTypeById({
slug: true,
description: true,
length: true,
offsetStart: true,
hidden: true,
locations: true,
eventName: true,
+4 -2
View File
@@ -21,8 +21,10 @@ export default function useTheme(themeToSet?: Maybe<string>) {
return;
}
// Embed theme takes precedence over theme configured in app. If embedTheme isn't set that is it's in 'Auto' mode, then it would use the theme configured in appearance.
const finalThemeToSet = embedTheme || themeToSet;
// Embed theme takes precedence over theme configured in app.
// If embedTheme isn't set i.e. it's not explicitly configured with a theme, then it would use the theme configured in appearance.
// If embedTheme is set to "auto" then we consider it as null which then uses system theme.
const finalThemeToSet = embedTheme ? (embedTheme === "auto" ? null : embedTheme) : themeToSet;
if (!finalThemeToSet || finalThemeToSet === activeTheme) return;
+15 -1
View File
@@ -86,5 +86,19 @@ export function useTypedQuery<T extends z.AnyZodObject>(schema: T) {
}
}
return { data: parsedQuery, setQuery, removeByKey, pushItemToKey, removeItemByKeyAndValue };
// Remove all query params from the URL
function removeAllQueryParams() {
const { asPath } = router;
const newPath = asPath.split("?")[0];
router.replace(newPath, undefined, { shallow: true });
}
return {
data: parsedQuery,
setQuery,
removeByKey,
pushItemToKey,
removeItemByKeyAndValue,
removeAllQueryParams,
};
}
+13 -1
View File
@@ -12,17 +12,20 @@ export type GetSlots = {
dateOverrides?: DateOverride[];
minimumBookingNotice: number;
eventLength: number;
offsetStart: number;
organizerTimeZone: string;
};
export type TimeFrame = { userIds?: number[]; startTime: number; endTime: number };
const minimumOfOne = (input: number) => (input < 1 ? 1 : input);
const minimumOfZero = (input: number) => (input < 0 ? 0 : input);
function buildSlots({
startOfInviteeDay,
computedLocalAvailability,
frequency,
eventLength,
offsetStart,
startDate,
organizerTimeZone,
inviteeTimeZone,
@@ -32,6 +35,7 @@ function buildSlots({
startDate: Dayjs;
frequency: number;
eventLength: number;
offsetStart: number;
organizerTimeZone: string;
inviteeTimeZone: string;
}) {
@@ -42,6 +46,8 @@ function buildSlots({
// keep the old safeguards in; may be needed.
frequency = minimumOfOne(frequency);
eventLength = minimumOfOne(eventLength);
offsetStart = minimumOfZero(offsetStart);
// A day starts at 00:00 unless the startDate is the same as the current day
const dayStart = startOfInviteeDay.isSame(startDate, "day")
? Math.ceil((startDate.hour() * 60 + startDate.minute()) / frequency) * frequency
@@ -84,7 +90,11 @@ function buildSlots({
for (const [boundaryStart, boundaryEnd] of ranges) {
// loop through the day, based on frequency.
for (let slotStart = boundaryStart; slotStart < boundaryEnd; slotStart += frequency) {
for (
let slotStart = boundaryStart + offsetStart;
slotStart < boundaryEnd;
slotStart += offsetStart + frequency
) {
computedLocalAvailability.forEach((item) => {
// TODO: This logic does not allow for past-midnight bookings.
if (slotStart < item.startTime || slotStart > item.endTime + 1 - eventLength) {
@@ -139,6 +149,7 @@ const getSlots = ({
workingHours,
dateOverrides = [],
eventLength,
offsetStart,
organizerTimeZone,
}: GetSlots) => {
// current date in invitee tz
@@ -240,6 +251,7 @@ const getSlots = ({
startDate,
frequency,
eventLength,
offsetStart,
organizerTimeZone,
inviteeTimeZone: timeZone,
});
+9 -1
View File
@@ -1,5 +1,13 @@
export const slugify = (str: string) => {
return str.replace(/[^a-zA-Z0-9-]/g, "-").toLowerCase();
return str
.toLowerCase() // Convert to lowercase
.trim() // Remove whitespace from both sides
.normalize("NFD") // Normalize to decomposed form for handling accents
.replace(/\p{Diacritic}/gu, "") // Remove any diacritics (accents) from characters
.replace(/[^\p{L}\p{N}\p{Zs}\p{Emoji}]+/gu, "-") // Replace any non-alphanumeric characters (including Unicode) with a dash
.replace(/[\s_]+/g, "-") // Replace whitespace and underscores with a single dash
.replace(/^-+/, "") // Remove dashes from start
.replace(/-+$/, ""); // Remove dashes from end
};
export default slugify;
+1
View File
@@ -71,6 +71,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
position: 1,
locations: null,
length: 15,
offsetStart: 0,
hidden: false,
userId: null,
teamId: null,
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "offsetStart" INTEGER NOT NULL DEFAULT 0;
+1
View File
@@ -57,6 +57,7 @@ model EventType {
/// @zod.custom(imports.eventTypeLocations)
locations Json?
length Int
offsetStart Int @default(0)
hidden Boolean @default(false)
hosts Host[]
users User[] @relation("user_eventtype")
+1
View File
@@ -65,6 +65,7 @@ export const availiblityPageEventTypeSelect = Prisma.validator<Prisma.EventTypeS
availability: true,
description: true,
length: true,
offsetStart: true,
price: true,
currency: true,
periodType: true,
+1
View File
@@ -501,6 +501,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
price: true,
slug: true,
length: true,
offsetStart: true,
locations: true,
hidden: true,
availability: true,
@@ -0,0 +1,13 @@
import { trpc } from "../trpc";
export function useAvatarQuery() {
const avatarQuery = trpc.viewer.avatar.useQuery(undefined, {
retry(failureCount) {
return failureCount > 3;
},
});
return avatarQuery;
}
export default useAvatarQuery;
@@ -51,11 +51,15 @@ export const getHandler = async ({ ctx, input }: GetOptions) => {
userId: user.id,
},
});
// disabling utc casting while fetching WorkingHours
return {
id: schedule.id,
name: schedule.name,
isManaged: schedule.userId !== user.id,
workingHours: getWorkingHours({ timeZone: schedule.timeZone || undefined }, schedule.availability || []),
workingHours: getWorkingHours(
{ timeZone: schedule.timeZone || undefined, utcOffset: 0 },
schedule.availability || []
),
schedule: schedule.availability,
availability: convertScheduleToAvailability(schedule).map((a) =>
a.map((startAndEnd) => ({
@@ -45,6 +45,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
// eslint-disable-next-line
teamId,
bookingFields,
offsetStart,
...rest
} = input;
@@ -102,6 +103,13 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
data.durationLimits = durationLimits;
}
if (offsetStart !== undefined) {
if (offsetStart < 0) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Offset start time must be zero or greater." });
}
data.offsetStart = offsetStart;
}
if (schedule) {
// Check that the schedule belongs to the user
const userScheduleQuery = await ctx.prisma.schedule.findFirst({
@@ -21,7 +21,7 @@ interface ReserveSlotOptions {
export const reserveSlotHandler = async ({ ctx, input }: ReserveSlotOptions) => {
const { prisma, req, res } = ctx;
const uid = req?.cookies?.uid || uuid();
const { slotUtcStartDate, slotUtcEndDate, eventTypeId, bookingAttendees } = input;
const { slotUtcStartDate, slotUtcEndDate, eventTypeId, bookingUid } = input;
const releaseAt = dayjs.utc().add(parseInt(MINUTES_TO_BOOK), "minutes").format();
const eventType = await prisma.eventType.findUnique({
where: { id: eventTypeId },
@@ -40,8 +40,13 @@ export const reserveSlotHandler = async ({ ctx, input }: ReserveSlotOptions) =>
// If this is a seated event then don't reserve a slot
if (eventType.seatsPerTimeSlot) {
// Check to see if this is the last attendee
if (bookingAttendees) {
const seatsLeft = eventType.seatsPerTimeSlot - bookingAttendees;
const bookingWithAttendees = await prisma.booking.findFirst({
where: { uid: bookingUid },
select: { attendees: true },
});
const bookingAttendeesLength = bookingWithAttendees?.attendees?.length;
if (bookingAttendeesLength) {
const seatsLeft = eventType.seatsPerTimeSlot - bookingAttendeesLength;
if (seatsLeft < 1) shouldReserveSlot = false;
} else {
// If there is no booking yet then don't reserve the slot
@@ -33,7 +33,7 @@ export const reserveSlotSchema = z
slotUtcStartDate: z.string(),
// endTime ISOString
slotUtcEndDate: z.string(),
bookingAttendees: z.number().optional(),
bookingUid: z.string().optional(),
})
.refine(
(data) => !!data.eventTypeId || !!data.slotUtcStartDate || !!data.slotUtcEndDate,
@@ -80,6 +80,7 @@ export async function getEventType(input: TGetScheduleInputSchema) {
slug: true,
minimumBookingNotice: true,
length: true,
offsetStart: true,
seatsPerTimeSlot: true,
timeZone: true,
slotInterval: true,
@@ -281,6 +282,7 @@ export async function getSchedule(input: TGetScheduleInputSchema) {
workingHours,
dateOverrides,
minimumBookingNotice: eventType.minimumBookingNotice,
offsetStart: eventType.offsetStart,
frequency: eventType.slotInterval || input.duration || eventType.length,
organizerTimeZone:
eventType.timeZone || eventType?.schedule?.timeZone || userAvailability?.[0]?.timeZone,
@@ -135,7 +135,6 @@ export const inviteMemberHandler = async ({ ctx, input }: InviteMemberOptions) =
inviteTeamOptions.isCalcomMember = false;
}
await sendTeamInviteEmail({
language: translation,
from: ctx.user.name,
@@ -10,12 +10,14 @@ import {
scheduleSMSReminder,
} from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import { IS_SELF_HOSTED, SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import type { PrismaClient } from "@calcom/prisma/client";
import { BookingStatus, WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { TrpcSessionUser } from "@calcom/trpc/server/trpc";
import { TRPCError } from "@trpc/server";
import { hasTeamPlanHandler } from "../teams/hasTeamPlan.handler";
import type { TUpdateInputSchema } from "./update.schema";
import {
getSender,
@@ -64,6 +66,15 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const isCurrentUsernamePremium = user && user.metadata && hasKeyInMetadata(user, "isPremium");
let isTeamsPlan = false;
if (!isCurrentUsernamePremium) {
const { hasTeamPlan } = await hasTeamPlanHandler({ ctx });
isTeamsPlan = !!hasTeamPlan;
}
const hasPaidPlan = IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan;
const activeOnEventTypes = await ctx.prisma.eventType.findMany({
where: {
id: {
@@ -377,13 +388,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
//step was edited
} else if (JSON.stringify(oldStep) !== JSON.stringify(newStep)) {
if (
!userWorkflow.teamId &&
!userWorkflow.user?.teams.length &&
!isSMSAction(oldStep.action) &&
isSMSAction(newStep.action) &&
!IS_SELF_HOSTED
) {
if (!hasPaidPlan && !isSMSAction(oldStep.action) && isSMSAction(newStep.action)) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
await ctx.prisma.workflowStep.update({
@@ -536,7 +541,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
//added steps
const addedSteps = steps.map((s) => {
if (s.id <= 0) {
if (!userWorkflow.user?.teams.length && isSMSAction(s.action) && !IS_SELF_HOSTED) {
if (isSMSAction(s.action) && !hasPaidPlan) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const { id: _stepId, ...stepToAdd } = s;
+3
View File
@@ -126,6 +126,9 @@ export interface App {
/** only required for "usage-based" billing. % of commission for paid bookings */
commission?: number;
licenseRequired?: boolean;
teamsPlanRequired?: {
upgradeUrl: string;
};
isProOnly?: boolean;
appData?: AppData;
/**
+17 -9
View File
@@ -1,9 +1,8 @@
import { CheckCircleIcon, ExclamationIcon, InformationCircleIcon, XCircleIcon } from "@heroicons/react/solid";
import classNames from "classnames";
import type { ReactNode } from "react";
import { forwardRef } from "react";
import { Info } from "../icon";
import { CheckCircle2, Info, XCircle, AlertTriangle } from "@calcom/ui/components/icon";
export interface AlertProps {
title?: ReactNode;
@@ -34,25 +33,34 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>((props, ref) => {
<div className="relative flex flex-col md:flex-row">
<div className="flex-shrink-0">
{severity === "error" && (
<XCircleIcon className={classNames("h-5 w-5 text-red-400", iconClassName)} aria-hidden="true" />
<XCircle
className={classNames("h-5 w-5 fill-red-400 text-white", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "warning" && (
<ExclamationIcon
className={classNames("h-5 w-5 text-yellow-400", iconClassName)}
<AlertTriangle
className={classNames("h-5 w-5 fill-yellow-400 text-white", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "info" && (
<InformationCircleIcon
className={classNames("h-5 w-5 text-sky-400", iconClassName)}
<Info
className={classNames("h-5 w-5 fill-sky-400 text-white", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "neutral" && (
<Info className={classNames("text-default h-5 w-5", iconClassName)} aria-hidden="true" />
<Info
className={classNames("text-default h-5 w-5 fill-transparent", iconClassName)}
aria-hidden="true"
/>
)}
{severity === "success" && (
<CheckCircleIcon className={classNames("text-muted h-5 w-5", iconClassName)} aria-hidden="true" />
<CheckCircle2
className={classNames("fill-muted h-5 w-5 text-white", iconClassName)}
aria-hidden="true"
/>
)}
</div>
<div className="ml-3 flex-grow">
+2
View File
@@ -97,6 +97,7 @@ export function AppCard({ app, credentials, searchText }: AppCardProps) {
<InstallAppButton
type={app.type}
isProOnly={app.isProOnly}
teamsPlanRequired={app.teamsPlanRequired}
disableInstall={!!app.dependencies && !app.dependencyData?.some((data) => !data.installed)}
wrapperClassName="[@media(max-width:260px)]:w-full"
render={({ useDefaultComponent, ...props }) => {
@@ -127,6 +128,7 @@ export function AppCard({ app, credentials, searchText }: AppCardProps) {
isProOnly={app.isProOnly}
wrapperClassName="[@media(max-width:260px)]:w-full"
disableInstall={!!app.dependencies && app.dependencyData?.some((data) => !data.installed)}
teamsPlanRequired={app.teamsPlanRequired}
render={({ useDefaultComponent, ...props }) => {
if (useDefaultComponent) {
props = {
@@ -1,8 +1,8 @@
.react-calendar {
width: 350px;
max-width: 100%;
background: white;
border: 1px solid #e5e7eb;
background: var(--cal-bg);
border: 1px solid var(--cal-bg-inverted);
font-family: Arial, Helvetica, sans-serif;
line-height: 1.125em;
}
@@ -52,13 +52,13 @@
}
.react-calendar__navigation button:disabled {
background-color: #f0f0f0;
background-color: transparent;
}
.react-calendar__navigation button:enabled:hover,
.react-calendar__navigation button:enabled:focus {
background-color: #e5e7eb;
color: #111827;
background-color: #1d5ad8;
color: var(--cal-text-emphasis);
}
.react-calendar__tile {
@@ -100,7 +100,7 @@
.react-calendar__tile:enabled:hover,
.react-calendar__tile:enabled:focus {
background-color: #e6e6e6;
background-color: var(--cal-bg-subtle);
}
.react-daterange-picker > .react-daterange-picker__wrapper {
@@ -134,7 +134,7 @@
.react-daterange-picker > .react-daterange-picker__wrapper:focus-within,
.react-daterange-picker > .react-daterange-picker__wrapper:focus-within:hover {
border-color: #111827;
border-color: var(--cal-bg-subtle);
border-width: 2px;
}
@@ -173,7 +173,7 @@
display: flex;
padding-left: 0.5rem;
padding-right: 0.5rem;
color: #111827;
color: var(--cal-text);
order: -9999;
justify-content: flex-start;
align-items: center;
@@ -206,26 +206,26 @@ button.react-calendar__tile.react-calendar__month-view__days__day {
}
.react-calendar__tile.react-calendar__tile--hover:not(.react-calendar__tile--active) {
background-color: #e5e7eb !important;
background-color: var(--cal-bg-subtle) !important;
}
.react-calendar__month-view__days > .react-calendar__tile.react-calendar__tile--hasActive,
.react-calendar__month-view__days > .react-calendar__tile.react-calendar__tile--active,
.react-calendar__month-view__days > button.react-calendar__tile.react-calendar__tile--active:hover {
background-color: #111827;
color: #ffffff;
background-color: var(--cal-bg-inverted);
color: var(--cal-text-inverted);
}
.react-calendar__tile.react-calendar__tile--active.react-calendar__month-view__days__day--weekend {
color: #ffffff;
color: var(--cal-text-inverted);
}
.react-calendar__tile.react-calendar__month-view__days__day--weekend {
color: #111827;
color: var(--cal-text-emphasis);
}
.react-calendar__tile.react-calendar__month-view__days__day--neighboringMonth {
color: #9ca3af;
color: var(--cal-text-muted);
}
.react-calendar__tile--now::before {
@@ -234,7 +234,7 @@ button.react-calendar__tile.react-calendar__month-view__days__day {
height: 4px;
border-radius: 50%;
position: absolute;
background-color: #111827;
background-color: var(--cal-text-emphasis);
left: 50%;
transform: translateX(-50%);
bottom: 0;
@@ -243,19 +243,19 @@ button.react-calendar__tile.react-calendar__month-view__days__day {
button.react-calendar__tile.react-calendar__month-view__days__day:hover,
.react-calendar__tile.react-calendar__year-view__months__month:hover {
background-color: #d1d5db;
color: #111827;
background-color: var(--cal-bg-subtle);
color: var(--cal-text-emphasis);
}
.react-daterange-picker > .react-daterange-picker__wrapper:hover {
border-color: #9ca3af;
border-color: var(--cal-bg-subtle);
}
.react-daterange-picker.react-daterange-picker--disabled > .react-daterange-picker__wrapper:hover {
border-color: #e5e7eb;
border-color: var(--cal-bg-subtle);
}
.react-calendar__navigation button.react-calendar__navigation__label:enabled:hover,
.react-calendar__navigation button.react-calendar__navigation__label:enabled:focus {
background-color: transparent;
}
}
@@ -1,4 +1,3 @@
import { CheckCircleIcon } from "@heroicons/react/outline";
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
import Link from "next/link";
import type { ComponentProps } from "react";
@@ -6,6 +5,7 @@ import { forwardRef } from "react";
import { classNames } from "@calcom/lib";
import type { SVGComponent } from "@calcom/types/SVGComponent";
import { CheckCircle } from "@calcom/ui/components/icon";
import type { ButtonColor } from "../../button/Button";
@@ -76,7 +76,7 @@ export const DropdownMenuCheckboxItem = forwardRef<HTMLDivElement, DropdownMenuC
<DropdownMenuPrimitive.CheckboxItem {...props} ref={forwardedRef}>
{children}
<DropdownMenuPrimitive.ItemIndicator>
<CheckCircleIcon />
<CheckCircle />
</DropdownMenuPrimitive.ItemIndicator>
</DropdownMenuPrimitive.CheckboxItem>
);
@@ -93,7 +93,7 @@ export const DropdownMenuRadioItem = forwardRef<HTMLDivElement, DropdownMenuRadi
<DropdownMenuPrimitive.RadioItem {...props} ref={forwardedRef}>
{children}
<DropdownMenuPrimitive.ItemIndicator>
<CheckCircleIcon />
<CheckCircle />
</DropdownMenuPrimitive.ItemIndicator>
</DropdownMenuPrimitive.RadioItem>
);
+1 -1
View File
@@ -129,7 +129,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function
{addOnLeading || addOnSuffix ? (
<div
dir="ltr"
className="group relative mb-1 flex items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1">
className="group relative mb-1 flex max-w-max items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1">
{addOnLeading && (
<Addon isFilled={addOnFilled} className={classNames("rounded-l-md border-r-0", addOnClassname)}>
{addOnLeading}
@@ -54,11 +54,11 @@ export const Select = <
"bg-default border-default !min-h-9 h-9 text-sm leading-4 placeholder:text-sm placeholder:font-normal focus-within:ring-2 focus-within:ring-emphasis hover:border-emphasis rounded-md border ",
state.isMulti
? variant === "checkbox"
? "px-3 py-2"
? "px-3 py-2 h-fit"
: state.hasValue
? "p-1"
: "px-3 py-2"
: "py-2 px-3",
? "p-1 h-fit"
: "px-3 py-2 h-fit"
: "py-2 px-3 h-fit",
props.isDisabled && "bg-subtle",
props.classNames?.control
),

Some files were not shown because too many files have changed in this diff Show More