Merge branch 'main' into fix/engines-16

This commit is contained in:
Agusti Fernandez Pardo 2022-07-22 23:23:57 +02:00 committed by GitHub
commit edfffd6449
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
204 changed files with 2904 additions and 1463 deletions

View File

@ -127,3 +127,6 @@ EMAIL_SERVER_PASSWORD='<office365_password>'
## @see https://support.google.com/accounts/answer/185833
# EMAIL_SERVER_PASSWORD='<gmail_app_password>'
# **********************************************************************************************************
# Set the following value to true if you wish to enable Team Impersonation
NEXT_PUBLIC_TEAM_IMPERSONATION=false

View File

@ -10,5 +10,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
# Checking the actor will prevent your Action run failing on non-Dependabot
# PRs but also ensures that it only does work for Dependabot PRs.
if: ${{ github.actor == 'github-actions[bot]' }}
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"

@ -1 +1 @@
Subproject commit 8a796ab11dd6663be5e582e7bac49f0aabd92dc3
Subproject commit a26db083faaa79a40f96dddac888ba2c2bea921e

View File

@ -4,11 +4,11 @@ import { OptionProps } from "react-select";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App";
import { Button } from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
import { trpc } from "@lib/trpc";
interface AdditionalCalendarSelectorProps {
isLoading?: boolean;

View File

@ -16,12 +16,11 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App";
import { Button, SkeletonButton } from "@calcom/ui";
import LicenseRequired from "@ee/components/LicenseRequired";
import { trpc } from "@lib/trpc";
import Shell from "@components/Shell";
import Badge from "@components/ui/Badge";

View File

@ -3,8 +3,7 @@ import React, { useEffect, useState } from "react";
import Select from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
import { trpc } from "@calcom/trpc/react";
interface Props {
onChange: (value: { externalId: string; integration: string }) => void;

View File

@ -8,12 +8,12 @@ import { components, ControlProps } from "react-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { EventType } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Switch } from "@calcom/ui";
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
import { InputLeading, Label, TextArea, TextField } from "@calcom/ui/form/fields";
import { EMBED_LIB_URL, WEBAPP_URL } from "@lib/config/constants";
import { trpc } from "@lib/trpc";
import NavTabs from "@components/NavTabs";
import ColorPicker from "@components/ui/colorpicker";

View File

@ -1,7 +1,7 @@
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
import { trpc } from "@lib/trpc";
import { trpc } from "@calcom/trpc/react";
export function useViewerI18n() {
return trpc.useQuery(["viewer.public.i18n"], {

View File

@ -1,56 +0,0 @@
// This component is abstracted from /event-types/[type] for common usecase.
import { PencilIcon } from "@heroicons/react/solid";
import { useState } from "react";
export default function PencilEdit({
value,
// eslint-disable-next-line @typescript-eslint/no-empty-function
onChange = () => {},
placeholder = "",
readOnly = false,
}: {
value: string;
onChange?: (value: string) => void;
placeholder?: string;
readOnly?: boolean;
}) {
const [editIcon, setEditIcon] = useState(true);
const onDivClick = !readOnly
? () => {
return setEditIcon(false);
}
: // eslint-disable-next-line @typescript-eslint/no-empty-function
() => {};
return (
<div className="group relative min-h-[28px] cursor-pointer" onClick={onDivClick}>
{editIcon ? (
<>
<h1
style={{ fontSize: 22, letterSpacing: "-0.0009em" }}
className="inline-block pl-0 text-gray-900 focus:text-black group-hover:text-gray-500">
{value}
</h1>
{!readOnly ? (
<PencilIcon className="ml-1 -mt-1 inline h-4 w-4 text-gray-700 group-hover:text-gray-500" />
) : null}
</>
) : (
<div style={{ marginBottom: -11 }}>
<input
type="text"
autoFocus
style={{ top: -6, fontSize: 22 }}
required
className="relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
placeholder={placeholder}
defaultValue={value}
onBlur={(e) => {
setEditIcon(true);
onChange(e.target.value);
}}
/>
</div>
)}
</div>
);
}

View File

@ -24,6 +24,7 @@ import { Toaster } from "react-hot-toast";
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
import { WEBAPP_URL, JOIN_SLACK, ROADMAP } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import Dropdown, {
DropdownMenuContent,
@ -39,7 +40,6 @@ import ErrorBoundary from "@lib/ErrorBoundary";
import classNames from "@lib/classNames";
import { shouldShowOnboarding } from "@lib/getting-started";
import useMeQuery from "@lib/hooks/useMeQuery";
import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding";
import { KBarRoot, KBarContent, KBarTrigger } from "@components/Kbar";
@ -494,7 +494,17 @@ function UserDropdown({ small }: { small?: boolean }) {
const { t } = useLocale();
const query = useMeQuery();
const user = query.data;
useEffect(() => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-ignore
const Beacon = window.Beacon;
// window.Beacon is defined when user actually opens up HelpScout and username is available here. On every re-render update session info, so that it is always latest.
Beacon &&
Beacon("session-data", {
username: user?.username || "Unknown",
screenResolution: `${screen.width}x${screen.height}`,
});
});
const mutation = trpc.useMutation("viewer.away", {
onSettled() {
utils.invalidateQueries("viewer.me");
@ -570,7 +580,7 @@ function UserDropdown({ small }: { small?: boolean }) {
<DropdownMenuItem>
<a
onClick={() => {
mutation.mutate({ away: user?.away });
mutation.mutate({ away: !user?.away });
utils.invalidateQueries("viewer.me");
}}
className="flex min-w-max cursor-pointer px-4 py-2 text-sm hover:bg-gray-100 hover:text-gray-900">

View File

@ -1,9 +1,8 @@
import Link from "next/link";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { trpc } from "@lib/trpc";
import Badge from "@components/ui/Badge";
interface AppCardProps {

View File

@ -2,11 +2,11 @@ import { signIn } from "next-auth/react";
import { Dispatch, SetStateAction } from "react";
import { useFormContext } from "react-hook-form";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { useLocale } from "@lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
interface Props {
email: string;

View File

@ -4,12 +4,12 @@ import { useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
import { Form } from "@calcom/ui/form/fields";
import { HttpError } from "@lib/core/http/error";
import { trpc } from "@lib/trpc";
export function NewScheduleButton({ name = "new-schedule" }: { name?: string }) {
const router = useRouter();

View File

@ -8,7 +8,8 @@ import { GroupBase, Props } from "react-select";
import dayjs, { Dayjs, ConfigType } from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import Button from "@calcom/ui/Button";
import Dropdown, { DropdownMenuTrigger, DropdownMenuContent } from "@calcom/ui/Dropdown";
import Dropdown, { DropdownMenuContent } from "@calcom/ui/Dropdown";
import { Tooltip } from "@calcom/ui/Tooltip";
import { defaultDayRange } from "@lib/availability";
import { weekdayNames } from "@lib/core/i18n/weekday";
@ -202,7 +203,7 @@ export const DayRanges = ({
const { setValue, watch } = useFormContext();
// XXX: Hack to make copying times work; `fields` is out of date until save.
const watcher = watch(name);
const { t } = useLocale();
const { fields, replace, append, remove } = useFieldArray({
name,
});
@ -242,16 +243,18 @@ export const DayRanges = ({
</div>
{index === 0 && (
<div className="absolute top-2 right-0 text-right sm:relative sm:top-0 sm:flex-grow">
<Button
className="text-neutral-400"
type="button"
color="minimal"
size="icon"
StartIcon={PlusIcon}
onClick={handleAppend}
/>
<Tooltip content={t("add_time_availability") as string}>
<Button
className="text-neutral-400"
type="button"
color="minimal"
size="icon"
StartIcon={PlusIcon}
onClick={handleAppend}
/>
</Tooltip>
<Dropdown>
<DropdownMenuTrigger asChild>
<Tooltip content={t("duplicate") as string}>
<Button
type="button"
color="minimal"
@ -259,7 +262,7 @@ export const DayRanges = ({
StartIcon={DuplicateIcon}
onClick={handleAppend}
/>
</DropdownMenuTrigger>
</Tooltip>
<DropdownMenuContent>
<CopyTimes
disabled={[parseInt(name.substring(name.lastIndexOf(".") + 1), 10)]}

View File

@ -5,11 +5,10 @@ import { Fragment } from "react";
import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Availability } from "@calcom/prisma/client";
import { inferQueryOutput } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
import { inferQueryOutput } from "@lib/trpc";
export function ScheduleListItem({
schedule,
deleteFunction,

View File

@ -6,13 +6,12 @@ import { FC, useEffect, useState } from "react";
import dayjs, { Dayjs } from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { nameOfDay } from "@calcom/lib/weekday";
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
import classNames from "@lib/classNames";
import { timeZone } from "@lib/clock";
import type { Slot } from "@server/routers/viewer/slots";
type AvailableTimesProps = {
timeFormat: string;
eventTypeId: number;

View File

@ -18,6 +18,7 @@ import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { Tooltip } from "@calcom/ui/Tooltip";
@ -28,7 +29,6 @@ import useMeQuery from "@lib/hooks/useMeQuery";
import { linkValueToString } from "@lib/linkValueToString";
import { LocationType } from "@lib/location";
import { extractRecurringDates } from "@lib/parseDate";
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";

View File

@ -35,6 +35,7 @@ import { CAL_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { localStorage } from "@calcom/lib/webstorage";
import { trpc } from "@calcom/trpc/react";
import DatePicker from "@calcom/ui/booker/DatePicker";
import { timeZone as localStorageTimeZone } from "@lib/clock";
@ -44,7 +45,6 @@ import useTheme from "@lib/hooks/useTheme";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { trpc } from "@lib/trpc";
import CustomBranding from "@components/CustomBranding";
import AvailableTimes from "@components/booking/AvailableTimes";

View File

@ -8,6 +8,7 @@ import { z } from "zod";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { Form } from "@calcom/ui/form/fields";
@ -16,7 +17,6 @@ import { QueryCell } from "@lib/QueryCell";
import { linkValueToString } from "@lib/linkValueToString";
import { LocationType } from "@lib/location";
import { LocationOptionsToString } from "@lib/locationOptions";
import { inferQueryOutput, trpc } from "@lib/trpc";
import CheckboxField from "@components/ui/form/CheckboxField";
import type PhoneInputType from "@components/ui/form/PhoneInput";

View File

@ -5,12 +5,12 @@ import { useMutation } from "react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
import { TextArea } from "@calcom/ui/form/fields";
import * as fetchWrapper from "@lib/core/http/fetch-wrapper";
import { trpc } from "@lib/trpc";
interface IRescheduleDialog {
isOpenDialog: boolean;

View File

@ -6,9 +6,11 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import type { z } from "zod";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import { Button } from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent } from "@calcom/ui/Dialog";
@ -23,7 +25,6 @@ import { Form, InputLeading, TextAreaField, TextField } from "@calcom/ui/form/fi
import { HttpError } from "@lib/core/http/error";
import { slugify } from "@lib/slugify";
import { trpc } from "@lib/trpc";
import Avatar from "@components/ui/Avatar";
import * as RadioArea from "@components/ui/form/radio-area";
@ -164,7 +165,7 @@ export default function CreateEventTypeButton(props: Props) {
onSelect={() => openModal(option)}>
<Avatar
alt={option.name || ""}
imageSrc={option.image}
imageSrc={option.image || `${WEBAPP_URL}/${option.slug}/avatar.png`} // if no image, use default avatar
size={6}
className="inline ltr:mr-2 rtl:ml-2"
/>

View File

@ -4,12 +4,12 @@ import { useMutation } from "react-query";
import { InstallAppButton } from "@calcom/app-store/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import Switch from "@calcom/ui/Switch";
import { QueryCell } from "@lib/QueryCell";
import { inferQueryOutput, trpc } from "@lib/trpc";
import AdditionalCalendarSelector from "@components/AdditionalCalendarSelector";
import DestinationCalendarSelector from "@components/DestinationCalendarSelector";

View File

@ -3,11 +3,10 @@ import { useMutation } from "react-query";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { ButtonBaseProps } from "@calcom/ui/Button";
import { Dialog } from "@calcom/ui/Dialog";
import { trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
export default function DisconnectIntegration(props: {

View File

@ -1,9 +1,8 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { trpc } from "@lib/trpc";
import Badge from "@components/ui/Badge";
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {

View File

@ -0,0 +1,62 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import Badge from "@components/ui/Badge";
const DisableTeamImpersonation = ({ teamId, memberId }: { teamId: number; memberId: number }) => {
const { t } = useLocale();
const utils = trpc.useContext();
const query = trpc.useQuery(["viewer.teams.getMembershipbyUser", { teamId, memberId }]);
const mutation = trpc.useMutation("viewer.teams.updateMembership", {
onSuccess: async () => {
showToast(t("your_user_profile_updated_successfully"), "success");
await utils.invalidateQueries(["viewer.teams.getMembershipbyUser"]);
},
async onSettled() {
await utils.invalidateQueries(["viewer.public.i18n"]);
},
});
if (query.isLoading) return <></>;
return (
<>
<h3 className="font-cal mt-7 pb-4 text-xl leading-6 text-gray-900">{t("settings")}</h3>
<div className="-mx-0 rounded-sm border border-neutral-200 bg-white px-4 pb-4 sm:px-6">
<div className="flex flex-col justify-between pt-4 sm:flex-row">
<div>
<div className="flex flex-row items-center">
<h2 className="font-cal font-bold leading-6 text-gray-900">
{t("user_impersonation_heading")}
</h2>
<Badge
className="ml-2 text-xs"
variant={!query.data?.disableImpersonation ? "success" : "gray"}>
{!query.data?.disableImpersonation ? t("enabled") : t("disabled")}
</Badge>
</div>
<p className="text-sm text-gray-700">{t("team_impersonation_description")}</p>
</div>
<div className="mt-5 sm:mt-0 sm:self-center">
<Button
type="submit"
color="secondary"
onClick={() =>
!query.data?.disableImpersonation
? mutation.mutate({ teamId, memberId, disableImpersonation: true })
: mutation.mutate({ teamId, memberId, disableImpersonation: false })
}>
{!query.data?.disableImpersonation ? t("disable") : t("enable")}
</Button>
</div>
</div>
</div>
</>
);
};
export default DisableTeamImpersonation;

View File

@ -2,10 +2,9 @@ import { MembershipRole } from "@prisma/client";
import { SyntheticEvent, useMemo, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { trpc } from "@lib/trpc";
import ModalContainer from "@components/ui/ModalContainer";
import Select from "@components/ui/form/Select";

View File

@ -3,13 +3,13 @@ import { InformationCircleIcon } from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client";
import React, { useState, SyntheticEvent, useMemo } from "react";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
import { TextField } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
import { TeamWithMembers } from "@lib/queries/teams";
import { trpc } from "@lib/trpc";
import Select from "@components/ui/form/Select";

View File

@ -1,4 +1,4 @@
import { inferQueryOutput } from "@lib/trpc";
import { inferQueryOutput } from "@calcom/trpc/react";
import MemberListItem from "./MemberListItem";

View File

@ -1,12 +1,14 @@
import { PencilIcon, UserRemoveIcon } from "@heroicons/react/outline";
import { LockClosedIcon, PencilIcon, UserRemoveIcon } from "@heroicons/react/outline";
import { ClockIcon, DotsHorizontalIcon, ExternalLinkIcon } from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { useState } from "react";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import Dropdown, {
@ -19,7 +21,6 @@ import { Tooltip } from "@calcom/ui/Tooltip";
import TeamAvailabilityModal from "@ee/components/team/availability/TeamAvailabilityModal";
import useCurrentUserId from "@lib/hooks/useCurrentUserId";
import { inferQueryOutput, trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";
@ -39,6 +40,7 @@ export default function MemberListItem(props: Props) {
const utils = trpc.useContext();
const [showChangeMemberRoleModal, setShowChangeMemberRoleModal] = useState(false);
const [showTeamAvailabilityModal, setShowTeamAvailabilityModal] = useState(false);
const [showImpersonateModal, setShowImpersonateModal] = useState(false);
const removeMemberMutation = trpc.useMutation("viewer.teams.removeMember", {
async onSuccess() {
@ -147,6 +149,24 @@ export default function MemberListItem(props: Props) {
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
{/* Only show impersonate box if - The user has impersonation enabled,
They have accepted the team invite, and it is enabled for this instance */}
{!props.member.disableImpersonation &&
props.member.accepted &&
process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "true" && (
<>
<DropdownMenuItem>
<Button
onClick={() => setShowImpersonateModal(true)}
color="minimal"
StartIcon={LockClosedIcon}
className="w-full flex-shrink-0 font-normal">
{t("impersonate")}
</Button>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
</>
)}
<DropdownMenuItem>
<Dialog>
<DialogTrigger asChild>
@ -185,6 +205,39 @@ export default function MemberListItem(props: Props) {
onExit={() => setShowChangeMemberRoleModal(false)}
/>
)}
{showImpersonateModal && props.member.username && (
<ModalContainer isOpen={showImpersonateModal} onExit={() => setShowImpersonateModal(false)}>
<>
<div className="mb-4 sm:flex sm:items-start">
<div className="text-center sm:text-left">
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
{t("impersonate")}
</h3>
</div>
</div>
<form
onSubmit={async (e) => {
e.preventDefault();
await signIn("impersonation-auth", {
username: props.member.username,
teamId: props.team.id,
});
}}>
<p className="mt-2 text-sm text-gray-500" id="email-description">
{t("impersonate_user_tip")}
</p>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" color="primary" className="ltr:ml-2 rtl:mr-2">
{t("impersonate")}
</Button>
<Button type="button" color="secondary" onClick={() => setShowImpersonateModal(false)}>
{t("cancel")}
</Button>
</div>
</form>
</>
</ModalContainer>
)}
{showTeamAvailabilityModal && (
<ModalContainer
wide

View File

@ -2,12 +2,11 @@ import { UsersIcon } from "@heroicons/react/outline";
import { useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert";
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
import { trpc } from "@lib/trpc";
interface Props {
isOpen: boolean;
onClose: () => void;

View File

@ -1,6 +1,5 @@
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@lib/trpc";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import TeamListItem from "./TeamListItem";

View File

@ -13,6 +13,7 @@ import Link from "next/link";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import Dropdown, {
@ -25,7 +26,6 @@ import { Tooltip } from "@calcom/ui/Tooltip";
import classNames from "@lib/classNames";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import { inferQueryOutput, trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";

View File

@ -4,13 +4,12 @@ import React, { useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { objectKeys } from "@calcom/lib/objectKeys";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { TextField } from "@calcom/ui/form/fields";
import { TeamWithMembers } from "@lib/queries/teams";
import { trpc } from "@lib/trpc";
import ImageUploader from "@components/ImageUploader";
import SettingInputContainer from "@components/ui/SettingInputContainer";

View File

@ -5,11 +5,11 @@ import { useRouter } from "next/router";
import React from "react";
import showToast from "@calcom/lib/notification";
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
import { trpc } from "@calcom/trpc/react";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { useLocale } from "@lib/hooks/useLocale";
import { TeamWithMembers } from "@lib/queries/teams";
import { trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import CreateEventTypeButton from "@components/eventtype/CreateEventType";

View File

@ -1,6 +1,7 @@
import { useState } from "react";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import {
@ -13,7 +14,6 @@ import {
} from "@calcom/ui/Dialog";
import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
interface Props {
teamId: number;

View File

@ -1,11 +1,11 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import * as Tooltip from "@radix-ui/react-tooltip";
import { Maybe } from "@calcom/trpc/server";
import classNames from "@lib/classNames";
import { defaultAvatarSrc } from "@lib/profile";
import { Maybe } from "@trpc/server";
export type AvatarProps = {
className?: string;
size?: number;

View File

@ -1,18 +1,31 @@
import { PencilIcon } from "@heroicons/react/solid";
import { useState } from "react";
const EditableHeading = ({ title, onChange }: { title: string; onChange: (value: string) => void }) => {
const [editIcon, setEditIcon] = useState(true);
const EditableHeading = ({
title,
onChange,
placeholder = "",
readOnly = false,
}: {
title: string;
onChange?: (value: string) => void;
placeholder?: string;
readOnly?: boolean;
}) => {
const [isEditing, setIsEditing] = useState(false);
const enableEditing = () => !readOnly && setIsEditing(true);
return (
<div className="group relative cursor-pointer" onClick={() => setEditIcon(false)}>
{editIcon ? (
<div className="group relative cursor-pointer" onClick={enableEditing}>
{!isEditing ? (
<>
<h1
style={{ fontSize: 22, letterSpacing: "-0.0009em" }}
className="inline pl-0 text-gray-900 focus:text-black group-hover:text-gray-500">
className="inline pl-0 normal-case text-gray-900 focus:text-black group-hover:text-gray-500">
{title}
</h1>
<PencilIcon className="ml-1 -mt-1 inline h-4 w-4 text-gray-700 group-hover:text-gray-500" />
{!readOnly ? (
<PencilIcon className="ml-1 -mt-1 inline h-4 w-4 text-gray-700 group-hover:text-gray-500" />
) : null}
</>
) : (
<div style={{ marginBottom: -11 }}>
@ -22,8 +35,12 @@ const EditableHeading = ({ title, onChange }: { title: string; onChange: (value:
style={{ top: -6, fontSize: 22 }}
required
className="relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
placeholder={placeholder}
defaultValue={title}
onChange={(e) => onChange(e.target.value)}
onBlur={(e) => {
setIsEditing(false);
onChange && onChange(e.target.value);
}}
/>
</div>
)}

View File

@ -7,15 +7,13 @@ import { fetchUsername } from "@calcom/lib/fetchUsername";
import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { User } from "@calcom/prisma/client";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog";
import { Input, Label } from "@calcom/ui/form/fields";
import { trpc } from "@lib/trpc";
import { AppRouter } from "@server/routers/_app";
import { TRPCClientErrorLike } from "@trpc/client";
export enum UsernameChangeStatusEnum {
NORMAL = "NORMAL",
UPGRADE = "UPGRADE",
@ -274,16 +272,14 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
<div className="flex-1 px-2">
<p className="text-gray-500">
{t("current")} {t("username")}
</p>
<p className="text-gray-500">{t("current_username")}</p>
<p className="mt-1" data-testid="current-username">
{currentUsername}
</p>
</div>
<div className="ml-6 flex-1">
<p className="text-gray-500" data-testid="new-username">
{t("new")} {t("username")}
{t("new_username")}
</p>
<p>{inputUsernameValue}</p>
</div>

View File

@ -5,15 +5,13 @@ import { MutableRefObject, useCallback, useEffect, useState } from "react";
import { fetchUsername } from "@calcom/lib/fetchUsername";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
import Button from "@calcom/ui/Button";
import { Dialog, DialogClose, DialogContent, DialogHeader } from "@calcom/ui/Dialog";
import { Input, Label } from "@calcom/ui/form/fields";
import { trpc } from "@lib/trpc";
import { AppRouter } from "@server/routers/_app";
import { TRPCClientErrorLike } from "@trpc/client";
interface ICustomUsernameProps {
currentUsername: string | undefined;
setCurrentUsername: (value: string | undefined) => void;
@ -144,7 +142,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
</div>
)}
</div>
<div className="xs:hidden">
<div className="hidden md:inline">
<ActionButtons index="desktop" />
</div>
</div>
@ -171,16 +169,14 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
<div className="flex w-full flex-wrap rounded-sm bg-gray-100 py-3 text-sm">
<div className="flex-1 px-2">
<p className="text-gray-500">
{t("current")} {t("username").toLocaleLowerCase()}
</p>
<p className="text-gray-500">{t("current_username")}</p>
<p className="mt-1" data-testid="current-username">
{currentUsername}
</p>
</div>
<div className="flex-1">
<p className="text-gray-500" data-testid="new-username">
{t("new")} {t("username").toLocaleLowerCase()}
{t("new_username")}
</p>
<p>{inputUsernameValue}</p>
</div>

View File

@ -3,14 +3,13 @@ import { Controller, useForm } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { Tooltip } from "@calcom/ui";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { DialogFooter } from "@calcom/ui/Dialog";
import Switch from "@calcom/ui/Switch";
import { FieldsetLegend, Form, InputGroupBox, TextArea, TextField } from "@calcom/ui/form/fields";
import { trpc } from "@lib/trpc";
import { WEBHOOK_TRIGGER_EVENTS } from "@lib/webhooks/constants";
import { WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP } from "@lib/webhooks/constants";
import customTemplate, { hasTemplateIntegration } from "@lib/webhooks/integrationTemplate";
import { TWebhook } from "@components/webhook/WebhookListItem";
@ -19,14 +18,20 @@ import WebhookTestDisclosure from "@components/webhook/WebhookTestDisclosure";
export default function WebhookDialogForm(props: {
eventTypeId?: number;
defaultValues?: TWebhook;
app?: string;
handleClose: () => void;
}) {
const { t } = useLocale();
const utils = trpc.useContext();
const appId = props.app;
const triggers = !appId
? WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP["core"]
: WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP[appId as keyof typeof WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP];
const {
defaultValues = {
id: "",
eventTriggers: WEBHOOK_TRIGGER_EVENTS,
eventTriggers: triggers,
subscriberUrl: "",
active: true,
payloadTemplate: null,
@ -60,8 +65,8 @@ export default function WebhookDialogForm(props: {
form={form}
handleSubmit={async (event) => {
const e = changeSecret
? { ...event, eventTypeId: props.eventTypeId }
: { ...event, secret: currentSecret, eventTypeId: props.eventTypeId };
? { ...event, eventTypeId: props.eventTypeId, appId }
: { ...event, secret: currentSecret, eventTypeId: props.eventTypeId, appId };
if (!useCustomPayloadTemplate && event.payloadTemplate) {
event.payloadTemplate = null;
}
@ -115,7 +120,7 @@ export default function WebhookDialogForm(props: {
<fieldset className="space-y-2">
<FieldsetLegend>{t("event_triggers")}</FieldsetLegend>
<InputGroupBox className="border-0 bg-gray-50">
{WEBHOOK_TRIGGER_EVENTS.map((key) => (
{triggers.map((key) => (
<Controller
key={key}
control={form.control}

View File

@ -1,11 +1,11 @@
import { PlusIcon } from "@heroicons/react/solid";
import { useState } from "react";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import { QueryCell } from "@lib/QueryCell";
import { trpc } from "@lib/trpc";
import { List } from "@components/List";
import { ShellSubHeading } from "@components/Shell";
@ -17,12 +17,16 @@ export type WebhookListContainerType = {
title: string;
subtitle: string;
eventTypeId?: number;
appId?: string;
};
export default function WebhookListContainer(props: WebhookListContainerType) {
const query = trpc.useQuery(["viewer.webhook.list", { eventTypeId: props.eventTypeId }], {
suspense: true,
});
const query = trpc.useQuery(
["viewer.webhook.list", { eventTypeId: props.eventTypeId, appId: props.appId }],
{
suspense: true,
}
);
const [newWebhookModal, setNewWebhookModal] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [editing, setEditing] = useState<TWebhook | null>(null);
@ -66,6 +70,7 @@ export default function WebhookListContainer(props: WebhookListContainerType) {
<Dialog open={newWebhookModal} onOpenChange={(isOpen) => !isOpen && setNewWebhookModal(false)}>
<DialogContent>
<WebhookDialogForm
app={props.appId}
eventTypeId={props.eventTypeId}
handleClose={() => setNewWebhookModal(false)}
/>
@ -76,6 +81,7 @@ export default function WebhookListContainer(props: WebhookListContainerType) {
<DialogContent>
{editing && (
<WebhookDialogForm
app={props.appId}
key={editing.id}
eventTypeId={props.eventTypeId || undefined}
handleClose={() => setEditModalOpen(false)}

View File

@ -1,12 +1,12 @@
import { PencilAltIcon, TrashIcon } from "@heroicons/react/outline";
import classNames from "@calcom/lib/classNames";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { Tooltip } from "@calcom/ui/Tooltip";
import { useLocale } from "@lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@lib/trpc";
import { ListItem } from "@components/List";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";

View File

@ -5,11 +5,11 @@ import { useWatch } from "react-hook-form";
import classNames from "@calcom/lib/classNames";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { InputGroupBox } from "@calcom/ui/form/fields";
import { useLocale } from "@lib/hooks/useLocale";
import { trpc } from "@lib/trpc";
export default function WebhookTestDisclosure() {
const subscriberUrl: string = useWatch({ name: "subscriberUrl" });

View File

@ -5,14 +5,13 @@ import { Controller, useForm } from "react-hook-form";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { DialogFooter } from "@calcom/ui/Dialog";
import Switch from "@calcom/ui/Switch";
import { Tooltip } from "@calcom/ui/Tooltip";
import { Form, TextField } from "@calcom/ui/form/fields";
import { trpc } from "@lib/trpc";
import { DatePicker } from "@components/ui/form/DatePicker";
import LicenseRequired from "../LicenseRequired";

View File

@ -2,13 +2,13 @@ import { PlusIcon } from "@heroicons/react/outline";
import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogContent } from "@calcom/ui/Dialog";
import ApiKeyDialogForm from "@ee/components/apiKeys/ApiKeyDialogForm";
import ApiKeyListItem, { TApiKeys } from "@ee/components/apiKeys/ApiKeyListItem";
import { QueryCell } from "@lib/QueryCell";
import { trpc } from "@lib/trpc";
import { List } from "@components/List";
import { ShellSubHeading } from "@components/Shell";

View File

@ -4,12 +4,11 @@ import { ExclamationIcon } from "@heroicons/react/solid";
import dayjs from "@calcom/dayjs";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { Tooltip } from "@calcom/ui/Tooltip";
import { inferQueryOutput, trpc } from "@lib/trpc";
import { ListItem } from "@components/List";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Badge from "@components/ui/Badge";

View File

@ -2,13 +2,13 @@ import React, { useEffect, useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { TextArea } from "@calcom/ui/form/fields";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Badge from "@components/ui/Badge";

View File

@ -4,10 +4,10 @@ import { HelpScout, useChat } from "react-live-chat-loader";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import classNames from "@lib/classNames";
import { trpc } from "@lib/trpc";
import ContactMenuItem from "./ContactMenuItem";

View File

@ -2,10 +2,9 @@ import React, { useState, useEffect } from "react";
import dayjs from "@calcom/dayjs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import LicenseRequired from "@ee/components/LicenseRequired";
import { inferQueryOutput, trpc } from "@lib/trpc";
import Avatar from "@components/ui/Avatar";
import { DatePicker } from "@components/ui/form/DatePicker";
import Select from "@components/ui/form/Select";

View File

@ -4,8 +4,7 @@ import { FixedSizeList as List } from "react-window";
import dayjs from "@calcom/dayjs";
import { CAL_URL } from "@calcom/lib/constants";
import { inferQueryOutput, trpc } from "@lib/trpc";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Avatar from "@components/ui/Avatar";
import { DatePicker } from "@components/ui/form/DatePicker";

View File

@ -3,9 +3,8 @@ import React from "react";
import { ITimezone } from "react-timezone-select";
import { Dayjs } from "@calcom/dayjs";
import getSlots from "@lib/slots";
import { trpc } from "@lib/trpc";
import getSlots from "@calcom/lib/slots";
import { trpc } from "@calcom/trpc/react";
import Loader from "@components/Loader";

View File

@ -9,6 +9,7 @@ import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
import { Form, TextField } from "@calcom/ui/form/fields";
@ -20,7 +21,6 @@ import {
} from "@ee/lib/workflows/getOptions";
import { HttpError } from "@lib/core/http/error";
import { trpc } from "@lib/trpc";
import PhoneInput from "@components/ui/form/PhoneInput";
import Select from "@components/ui/form/Select";

View File

@ -1,23 +1,22 @@
import { WorkflowActions, WorkflowTemplates } from "@prisma/client";
import { useRouter } from "next/router";
import { useState, useEffect, Dispatch, SetStateAction } from "react";
import { useState, Dispatch, SetStateAction, useMemo } from "react";
import { Controller, UseFormReturn } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Form } from "@calcom/ui/form/fields";
import { AddActionDialog } from "@ee/components/workflows/AddActionDialog";
import WorkflowStepContainer from "@ee/components/workflows/WorkflowStepContainer";
import { Option, FormValues } from "@ee/pages/workflows/[workflow]";
import { FormValues } from "@ee/pages/workflows/[workflow]";
import { trpc } from "@lib/trpc";
import MultiSelectCheckboxes from "@components/ui/form/MultiSelectCheckboxes";
import MultiSelectCheckboxes, { Option } from "@components/ui/form/MultiSelectCheckboxes";
interface Props {
form: UseFormReturn<FormValues, any>;
form: UseFormReturn<FormValues>;
workflowId: number;
selectedEventTypes: Option[];
setSelectedEventTypes: Dispatch<SetStateAction<Option[]>>;
@ -29,30 +28,31 @@ export default function WorkflowDetailsPage(props: Props) {
const router = useRouter();
const utils = trpc.useContext();
const [evenTypeOptions, setEventTypeOptions] = useState<Option[]>([]);
const [isAddActionDialogOpen, setIsAddActionDialogOpen] = useState(false);
const [reload, setReload] = useState(false);
const [editCounter, setEditCounter] = useState(0);
const { data, isLoading } = trpc.useQuery(["viewer.eventTypes"]);
useEffect(() => {
if (data) {
let options: Option[] = [];
data.eventTypeGroups.forEach((group) => {
const eventTypeOptions = group.eventTypes.map((eventType) => {
return { value: String(eventType.id), label: eventType.title };
});
options = [...options, ...eventTypeOptions];
});
setEventTypeOptions(options);
}
}, [isLoading]);
const eventTypeOptions = useMemo(
() =>
data?.eventTypeGroups.reduce(
(options, group) => [
...options,
...group.eventTypes.map((eventType) => ({
value: String(eventType.id),
label: eventType.title,
})),
],
[] as Option[]
) || [],
[data]
);
const updateMutation = trpc.useMutation("viewer.workflows.update", {
onSuccess: async ({ workflow }) => {
if (workflow) {
await utils.setQueryData(["viewer.workflows.get", { id: +workflow.id }], workflow);
utils.setQueryData(["viewer.workflows.get", { id: +workflow.id }], workflow);
showToast(
t("workflow_updated_successfully", {
@ -74,7 +74,7 @@ export default function WorkflowDetailsPage(props: Props) {
const addAction = (action: WorkflowActions, sendTo?: string) => {
const steps = form.getValues("steps");
const id =
steps && steps.length > 0
steps?.length > 0
? steps.sort((a, b) => {
return a.id - b.id;
})[0].id - 1
@ -130,7 +130,7 @@ export default function WorkflowDetailsPage(props: Props) {
render={() => {
return (
<MultiSelectCheckboxes
options={evenTypeOptions}
options={eventTypeOptions}
isLoading={isLoading}
setSelected={setSelectedEventTypes}
selected={selectedEventTypes}

View File

@ -6,13 +6,13 @@ import { useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { EventType, Workflow, WorkflowsOnEventTypes } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Tooltip } from "@calcom/ui";
import { Dialog } from "@calcom/ui/Dialog";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
import EmptyScreen from "@calcom/ui/EmptyScreen";
import { HttpError } from "@lib/core/http/error";
import { trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";

View File

@ -11,6 +11,8 @@ import { Dispatch, SetStateAction, useState } from "react";
import { Controller, UseFormReturn } from "react-hook-form";
import PhoneInput from "react-phone-number-input";
import classNames from "@calcom/lib/classNames";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
import Select from "@calcom/ui/form/Select";
@ -23,12 +25,9 @@ import {
} from "@ee/lib/workflows/getOptions";
import { FormValues } from "@ee/pages/workflows/[workflow]";
import classNames from "@lib/classNames";
import { useLocale } from "@lib/hooks/useLocale";
type WorkflowStepProps = {
step?: WorkflowStep;
form: UseFormReturn<FormValues, any>;
form: UseFormReturn<FormValues>;
reload?: boolean;
setReload?: Dispatch<SetStateAction<boolean>>;
editCounter: number;

View File

@ -1,66 +1,136 @@
import { User } from "@prisma/client";
import CredentialsProvider from "next-auth/providers/credentials";
import { getSession } from "next-auth/react";
import { asNumberOrThrow } from "@lib/asStringOrNull";
import prisma from "@lib/prisma";
const auditAndReturnNextUser = async (
impersonatedUser: Pick<User, "id" | "username" | "email" | "name" | "role">,
impersonatedByUID: number
) => {
// Log impersonations for audit purposes
await prisma.impersonations.create({
data: {
impersonatedBy: {
connect: {
id: impersonatedByUID,
},
},
impersonatedUser: {
connect: {
id: impersonatedUser.id,
},
},
},
});
const obj = {
id: impersonatedUser.id,
username: impersonatedUser.username,
email: impersonatedUser.email,
name: impersonatedUser.name,
role: impersonatedUser.role,
impersonatedByUID,
};
return obj;
};
const ImpersonationProvider = CredentialsProvider({
id: "impersonation-auth",
name: "Impersonation",
type: "credentials",
credentials: {
username: { label: "Username", type: "text " },
username: { type: "text" },
teamId: { type: "text" },
},
async authorize(creds, req) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore need to figure out how to correctly type this
const session = await getSession({ req });
if (session?.user.role !== "ADMIN") {
throw new Error("You do not have permission to do this.");
}
const teamId = creds?.teamId ? asNumberOrThrow(creds.teamId) : undefined;
if (session?.user.username === creds?.username) {
throw new Error("You cannot impersonate yourself.");
}
const user = await prisma.user.findUnique({
if (!creds?.username) throw new Error("Username must be present");
// If you are an ADMIN we return way before team impersonation logic is executed, so NEXT_PUBLIC_TEAM_IMPERSONATION certainly true
if (session?.user.role !== "ADMIN" && process.env.NEXT_PUBLIC_TEAM_IMPERSONATION === "false") {
throw new Error("You do not have permission to do this.");
}
// Get user who is being impersonated
const impersonatedUser = await prisma.user.findUnique({
where: {
username: creds?.username,
},
});
if (!user) {
throw new Error("This user does not exist");
}
if (user.disableImpersonation) {
throw new Error("This user has disabled Impersonation.");
}
// Log impersonations for audit purposes
await prisma.impersonations.create({
data: {
impersonatedBy: {
connect: {
id: session.user.id,
select: {
id: true,
username: true,
role: true,
name: true,
email: true,
disableImpersonation: true,
teams: {
where: {
disableImpersonation: false, // Ensure they have impersonation enabled
accepted: true, // Ensure they are apart of the team and not just invited.
team: {
id: teamId, // Bring back only the right team
},
},
},
impersonatedUser: {
connect: {
id: user.id,
select: {
teamId: true,
disableImpersonation: true,
},
},
},
});
const obj = {
id: user.id,
username: user.username,
email: user.email,
name: user.name,
role: user.role,
impersonatedByUID: session?.user.id,
};
return obj;
// Check if impersonating is allowed for this user
if (!impersonatedUser) {
throw new Error("This user does not exist");
}
if (session?.user.role === "ADMIN") {
if (impersonatedUser.disableImpersonation) {
throw new Error("This user has disabled Impersonation.");
}
return auditAndReturnNextUser(impersonatedUser, session?.user.id as number);
}
// Check session
const sessionUserFromDb = await prisma.user.findUnique({
where: {
id: session?.user.id,
},
include: {
teams: {
where: {
AND: [
{
role: {
in: ["ADMIN", "OWNER"],
},
},
{
team: {
id: teamId,
},
},
],
},
},
},
});
if (sessionUserFromDb?.teams.length === 0 || impersonatedUser.teams.length === 0) {
throw new Error("You do not have permission to do this.");
}
return auditAndReturnNextUser(impersonatedUser, session?.user.id as number);
},
});

View File

@ -1,181 +0,0 @@
import { PaymentType, Prisma } from "@prisma/client";
import Stripe from "stripe";
import { v4 as uuidv4 } from "uuid";
import { z } from "zod";
import getAppKeysFromSlug from "@calcom/app-store/_utils/getAppKeysFromSlug";
import { sendAwaitingPaymentEmail, sendOrganizerPaymentRefundFailedEmail } from "@calcom/emails";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
import { createPaymentLink } from "@calcom/stripe/client";
import stripe, { PaymentData } from "@calcom/stripe/server";
import { CalendarEvent } from "@calcom/types/Calendar";
const stripeKeysSchema = z.object({
payment_fee_fixed: z.number(),
payment_fee_percentage: z.number(),
});
const stripeCredentialSchema = z.object({
stripe_user_id: z.string(),
stripe_publishable_key: z.string(),
});
export async function handlePayment(
evt: CalendarEvent,
selectedEventType: {
price: number;
currency: string;
},
stripeCredential: { key: Prisma.JsonValue },
booking: {
user: { email: string | null; name: string | null; timeZone: string } | null;
id: number;
startTime: { toISOString: () => string };
uid: string;
}
) {
const appKeys = await getAppKeysFromSlug("stripe");
const { payment_fee_fixed, payment_fee_percentage } = stripeKeysSchema.parse(appKeys);
const paymentFee = Math.round(selectedEventType.price * payment_fee_percentage + payment_fee_fixed);
const { stripe_user_id, stripe_publishable_key } = stripeCredentialSchema.parse(stripeCredential.key);
const params: Stripe.PaymentIntentCreateParams = {
amount: selectedEventType.price,
currency: selectedEventType.currency,
payment_method_types: ["card"],
application_fee_amount: paymentFee,
};
const paymentIntent = await stripe.paymentIntents.create(params, { stripeAccount: stripe_user_id });
const payment = await prisma.payment.create({
data: {
type: PaymentType.STRIPE,
uid: uuidv4(),
booking: {
connect: {
id: booking.id,
},
},
amount: selectedEventType.price,
fee: paymentFee,
currency: selectedEventType.currency,
success: false,
refunded: false,
data: Object.assign({}, paymentIntent, {
stripe_publishable_key,
stripeAccount: stripe_user_id,
}) /* We should treat this */ as PaymentData /* but Prisma doesn't know how to handle it, so it we treat it */ as unknown /* and then */ as Prisma.InputJsonValue,
externalId: paymentIntent.id,
},
});
await sendAwaitingPaymentEmail({
...evt,
paymentInfo: {
link: createPaymentLink({
paymentUid: payment.uid,
name: booking.user?.name,
email: booking.user?.email,
date: booking.startTime.toISOString(),
}),
},
});
return payment;
}
export async function refund(
booking: {
id: number;
uid: string;
startTime: Date;
payment: {
id: number;
success: boolean;
refunded: boolean;
externalId: string;
data: Prisma.JsonValue;
type: PaymentType;
}[];
},
calEvent: CalendarEvent
) {
try {
const payment = booking.payment.find((e) => e.success && !e.refunded);
if (!payment) return;
if (payment.type !== PaymentType.STRIPE) {
await handleRefundError({
event: calEvent,
reason: "cannot refund non Stripe payment",
paymentId: "unknown",
});
return;
}
const refund = await stripe.refunds.create(
{
payment_intent: payment.externalId,
},
{ stripeAccount: (payment.data as unknown as PaymentData)["stripeAccount"] }
);
if (!refund || refund.status === "failed") {
await handleRefundError({
event: calEvent,
reason: refund?.failure_reason || "unknown",
paymentId: payment.externalId,
});
return;
}
await prisma.payment.update({
where: {
id: payment.id,
},
data: {
refunded: true,
},
});
} catch (e) {
const err = getErrorFromUnknown(e);
console.error(err, "Refund failed");
await handleRefundError({
event: calEvent,
reason: err.message || "unknown",
paymentId: "unknown",
});
}
}
export const closePayments = async (paymentIntentId: string, stripeAccount: string) => {
try {
// Expire all current sessions
const sessions = await stripe.checkout.sessions.list(
{
payment_intent: paymentIntentId,
},
{ stripeAccount }
);
for (const session of sessions.data) {
await stripe.checkout.sessions.expire(session.id, { stripeAccount });
}
// Then cancel the payment intent
await stripe.paymentIntents.cancel(paymentIntentId, { stripeAccount });
return;
} catch (e) {
console.error(e);
return;
}
};
async function handleRefundError(opts: { event: CalendarEvent; reason: string; paymentId: string }) {
console.error(`refund failed: ${opts.reason} for booking '${opts.event.uid}'`);
await sendOrganizerPaymentRefundFailedEmail({
...opts.event,
paymentInfo: { reason: opts.reason, id: opts.paymentId },
});
}

View File

@ -9,7 +9,6 @@ import client from "@sendgrid/client";
import sgMail from "@sendgrid/mail";
import dayjs from "@calcom/dayjs";
import { sendWorkflowReminderEmail } from "@calcom/emails";
import prisma from "@calcom/prisma";
import { BookingInfo, timeUnitLowerCase } from "@ee/lib/workflows/reminders/smsReminderManager";
import emailReminderTemplate from "@ee/lib/workflows/reminders/templates/emailReminderTemplate";
@ -46,7 +45,10 @@ export const scheduleEmailReminder = async (
const scheduledDate =
timeBefore.time && timeUnit ? dayjs(startTime).subtract(timeBefore.time, timeUnit) : null;
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) return;
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
console.error("Sendgrid credentials are missing from the .env file");
return;
}
const batchIdResponse = await client.request({
url: "/v3/mail/batch",
@ -57,11 +59,17 @@ export const scheduleEmailReminder = async (
const attendeeName = action === WorkflowActions.EMAIL_HOST ? evt.attendees[0].name : evt.organizer.name;
const timeZone = action === WorkflowActions.EMAIL_HOST ? evt.organizer.timeZone : evt.attendees[0].timeZone;
let emailContent = {
emailSubject,
emailBody: {
text: emailBody,
html: `<body style="white-space: pre-wrap;">${emailBody}</body>`,
},
};
switch (template) {
case WorkflowTemplates.REMINDER:
const emailTemplate = emailReminderTemplate(startTime, evt.title, timeZone, attendeeName, name);
emailSubject = emailTemplate.subject;
emailBody = emailTemplate.body;
emailContent = emailReminderTemplate(startTime, evt.title, timeZone, attendeeName, name);
break;
}
@ -70,7 +78,14 @@ export const scheduleEmailReminder = async (
triggerEvent === WorkflowTriggerEvents.EVENT_CANCELLED
) {
try {
await sendWorkflowReminderEmail(evt, sendTo, emailSubject, emailBody);
await sgMail.send({
to: sendTo,
from: senderEmail,
subject: emailContent.emailSubject,
text: emailContent.emailBody.text,
html: emailContent.emailBody.html,
batchId: batchIdResponse[1].batch_id,
});
} catch (error) {
console.log("Error sending Email");
}
@ -85,13 +100,9 @@ export const scheduleEmailReminder = async (
await sgMail.send({
to: sendTo,
from: senderEmail,
subject: emailSubject,
content: [
{
type: "text/html",
value: emailBody,
},
],
subject: emailContent.emailSubject,
text: emailContent.emailBody.text,
html: emailContent.emailBody.html,
batchId: batchIdResponse[1].batch_id,
sendAt: scheduledDate.unix(),
});

View File

@ -4,7 +4,7 @@ import {
WorkflowTemplates,
WorkflowActions,
WorkflowMethods,
} from "@prisma/client/";
} from "@prisma/client";
import dayjs from "@calcom/dayjs";
import prisma from "@calcom/prisma";

View File

@ -7,19 +7,25 @@ const emailReminderTemplate = (
attendee: string,
name: string
) => {
const templateSubject = `Reminder: ${eventName} at ${dayjs(startTime)
const emailSubject = `Reminder: ${eventName} on ${dayjs(startTime)
.tz(timeZone)
.format("YYYY MMM D h:mmA")}`;
.format("YYYY MMM D")} at ${dayjs(startTime).tz(timeZone).format("h:mmA")} ${timeZone}.`;
const templateBody = `Hi ${name},\n\nThis is a reminder that your meeting (${eventName}) with ${attendee} is on ${dayjs(
const templateBodyText = `Hi ${name}, this is a reminder that your meeting (${eventName}) with ${attendee} is on ${dayjs(
startTime
)
.tz(timeZone)
.format("YYYY MMM D")} at ${dayjs(startTime).tz(timeZone).format("h:mmA")} ${timeZone}.`;
const emailContent = { subject: templateSubject, body: templateBody };
const templateBodyHtml = `<body>Hi ${name},<br><br>This is a reminder that your meeting (${eventName}) with ${attendee} is on ${dayjs(
startTime
)
.tz(timeZone)
.format("YYYY MMM D")} at ${dayjs(startTime).tz(timeZone).format("h:mmA")} ${timeZone}.<body>`;
return emailContent;
const emailBody = { text: templateBodyText, html: templateBodyHtml };
return { emailSubject, emailBody };
};
export default emailReminderTemplate;

View File

@ -72,11 +72,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
? reminder.booking?.user?.email
: reminder.booking?.attendees[0].email;
let emailTemplate = {
subject: reminder.workflowStep.emailSubject || "",
body: reminder.workflowStep.reminderBody || "",
};
const name =
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
? reminder.booking?.attendees[0].name
@ -92,9 +87,17 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
? reminder.booking?.attendees[0].timeZone
: reminder.booking?.user?.timeZone;
let emailContent = {
emailSubject: reminder.workflowStep.emailSubject || "",
emailBody: {
text: reminder.workflowStep.reminderBody || "",
html: `<body style="white-space: pre-wrap;">${reminder.workflowStep.reminderBody || ""}</body>`,
},
};
switch (reminder.workflowStep.template) {
case WorkflowTemplates.REMINDER:
emailTemplate = emailReminderTemplate(
emailContent = emailReminderTemplate(
reminder.booking?.startTime.toISOString() || "",
reminder.booking?.eventType?.title || "",
timeZone || "",
@ -103,17 +106,13 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
);
break;
}
if (emailTemplate.subject.length > 0 && emailTemplate.body.length > 0 && sendTo) {
if (emailContent.emailSubject.length > 0 && emailContent.emailBody.text.length > 0 && sendTo) {
await sgMail.send({
to: sendTo,
from: senderEmail,
subject: emailTemplate.subject,
content: [
{
type: "text/html",
value: emailTemplate.body,
},
],
subject: emailContent.emailSubject,
text: emailContent.emailBody.text,
html: emailContent.emailBody.html,
batchId: batchIdResponse[1].batch_id,
sendAt: dayjs(reminder.scheduledDate).unix(),
});

View File

@ -2,13 +2,13 @@ import { useRouter } from "next/router";
import { useMemo, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import LicenseRequired from "@ee/components/LicenseRequired";
import TeamAvailabilityScreen from "@ee/components/team/availability/TeamAvailabilityScreen";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import useMeQuery from "@lib/hooks/useMeQuery";
import { trpc } from "@lib/trpc";
import Loader from "@components/Loader";
import Shell from "@components/Shell";

View File

@ -9,6 +9,7 @@ import { useForm } from "react-hook-form";
import { z } from "zod";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Loader from "@calcom/ui/Loader";
import LicenseRequired from "@ee/components/LicenseRequired";
@ -21,14 +22,9 @@ import {
} from "@ee/lib/workflows/constants";
import useMeQuery from "@lib/hooks/useMeQuery";
import { trpc } from "@lib/trpc";
import Shell from "@components/Shell";
export type Option = {
value: string;
label: string;
};
import { Option } from "@components/ui/form/MultiSelectCheckboxes";
export type FormValues = {
name: string;
@ -39,6 +35,30 @@ export type FormValues = {
timeUnit?: TimeUnit;
};
const formSchema = z.object({
name: z.string(),
activeOn: z.object({ value: z.string(), label: z.string() }).array(),
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),
time: z.number().gte(0).optional(),
timeUnit: z.enum(TIME_UNIT).optional(),
steps: z
.object({
id: z.number(),
stepNumber: z.number(),
action: z.enum(WORKFLOW_ACTIONS),
workflowId: z.number(),
reminderBody: z.string().optional().nullable(),
emailSubject: z.string().optional().nullable(),
template: z.enum(WORKFLOW_TEMPLATES),
sendTo: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional()
.nullable(),
})
.array(),
});
function WorkflowPage() {
const { t } = useLocale();
const session = useSession();
@ -50,30 +70,6 @@ function WorkflowPage() {
const [selectedEventTypes, setSelectedEventTypes] = useState<Option[]>([]);
const [isAllDataLoaded, setIsAllDataLoaded] = useState(false);
const formSchema = z.object({
name: z.string(),
activeOn: z.object({ value: z.string(), label: z.string() }).array(),
trigger: z.enum(WORKFLOW_TRIGGER_EVENTS),
time: z.number().gte(0).optional(),
timeUnit: z.enum(TIME_UNIT).optional(),
steps: z
.object({
id: z.number(),
stepNumber: z.number(),
action: z.enum(WORKFLOW_ACTIONS),
workflowId: z.number(),
reminderBody: z.string().optional().nullable(),
emailSubject: z.string().optional().nullable(),
template: z.enum(WORKFLOW_TEMPLATES),
sendTo: z
.string()
.refine((val) => isValidPhoneNumber(val))
.optional()
.nullable(),
})
.array(),
});
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
});
@ -82,6 +78,8 @@ function WorkflowPage() {
const {
data: workflow,
isError,
error,
isLoading,
dataUpdatedAt,
} = trpc.useQuery([
@ -113,15 +111,12 @@ function WorkflowPage() {
}
}, [dataUpdatedAt]);
if (isLoading) {
return <Loader />;
}
return (
<Shell
title="Title"
heading={
session.data?.hasValidLicense && (
session.data?.hasValidLicense &&
isAllDataLoaded && (
<div className="group relative cursor-pointer" onClick={() => setEditIcon(false)}>
{editIcon ? (
<>
@ -160,15 +155,21 @@ function WorkflowPage() {
<Alert className="border " severity="warning" title={t("pro_feature_workflows")} />
) : (
<>
{isAllDataLoaded ? (
<WorkflowDetailsPage
form={form}
workflowId={+workflowId}
selectedEventTypes={selectedEventTypes}
setSelectedEventTypes={setSelectedEventTypes}
/>
{!isError ? (
<>
{isAllDataLoaded ? (
<WorkflowDetailsPage
form={form}
workflowId={+workflowId}
selectedEventTypes={selectedEventTypes}
setSelectedEventTypes={setSelectedEventTypes}
/>
) : (
<Loader />
)}
</>
) : (
<Loader />
<Alert severity="error" title="Something went wrong" message={error.message} />
)}
</>
)}

View File

@ -1,13 +1,13 @@
import { useSession } from "next-auth/react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import LicenseRequired from "@ee/components/LicenseRequired";
import { NewWorkflowButton } from "@ee/components/workflows/NewWorkflowButton";
import WorkflowList from "@ee/components/workflows/WorkflowListPage";
import useMeQuery from "@lib/hooks/useMeQuery";
import { trpc } from "@lib/trpc";
import Loader from "@components/Loader";
import Shell from "@components/Shell";
@ -26,7 +26,7 @@ function WorkflowsPage() {
<Shell
heading={t("workflows")}
subtitle={t("workflows_to_automate_notifications")}
CTA={session.data?.hasValidLicense ? <NewWorkflowButton /> : <></>}>
CTA={session.data?.hasValidLicense && !isFreeUser ? <NewWorkflowButton /> : <></>}>
<LicenseRequired>
{isLoading ? (
<Loader />

View File

@ -3,7 +3,7 @@ import type { Config } from "@jest/types";
const config: Config.InitialOptions = {
verbose: true,
roots: ["<rootDir>"],
testMatch: ["**/tests/**/*.+(ts|tsx|js)", "**/?(*.)+(spec|test).+(ts|tsx|js)"],
testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)"],
testPathIgnorePatterns: ["<rootDir>/.next", "<rootDir>/playwright/"],
transform: {
"^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }],

View File

@ -8,22 +8,19 @@ import {
UseQueryResult,
} from "react-query";
import { Alert } from "@calcom/ui/Alert";
import { trpc } from "@lib/trpc";
import Loader from "@components/Loader";
import type { AppRouter } from "@server/routers/_app";
import type { TRPCClientErrorLike } from "@trpc/client";
import type { UseTRPCQueryOptions } from "@trpc/react";
// import type { inferProcedures } from "@trpc/react/src/createReactQueryHooks";
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import type { UseTRPCQueryOptions } from "@calcom/trpc/react";
import type {
inferHandlerInput,
inferProcedureInput,
inferProcedureOutput,
ProcedureRecord,
} from "@trpc/server";
} from "@calcom/trpc/server";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Alert } from "@calcom/ui/Alert";
import Loader from "@components/Loader";
type ErrorLike = {
message: string;

View File

@ -1,15 +1,14 @@
import { SessionProvider } from "next-auth/react";
import { appWithTranslation } from "next-i18next";
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
import { ComponentProps, ReactNode, useMemo } from "react";
import { ComponentProps, ReactNode } from "react";
import { trpc } from "@calcom/trpc/react";
import DynamicHelpscoutProvider from "@ee/lib/helpscout/providerDynamic";
import DynamicIntercomProvider from "@ee/lib/intercom/providerDynamic";
import usePublicPage from "@lib/hooks/usePublicPage";
import { trpc } from "./trpc";
const I18nextAdapter = appWithTranslation<NextJsAppProps & { children: React.ReactNode }>(({ children }) => (
<>{children}</>
));

View File

@ -1,11 +1,11 @@
import parser from "accept-language-parser";
import { IncomingMessage } from "http";
import { Maybe } from "@calcom/trpc/server";
import { getSession } from "@lib/auth";
import prisma from "@lib/prisma";
import { Maybe } from "@trpc/server";
import { i18n } from "../../../next-i18next.config";
export function getLocaleFromHeaders(req: IncomingMessage): string {

View File

@ -1,4 +1,4 @@
import { trpc } from "../trpc";
import { trpc } from "@calcom/trpc/react";
export function useMeQuery() {
const meQuery = trpc.useQuery(["viewer.me"], {

View File

@ -2,8 +2,7 @@ import Head from "next/head";
import { useEffect, useState } from "react";
import { useEmbedTheme } from "@calcom/embed-core/embed-iframe";
import { Maybe } from "@trpc/server";
import { Maybe } from "@calcom/trpc/server";
// This method is stringified and executed only on client. So,
// - Pass all the params explicitly to this method. Don't use closure

View File

@ -1,41 +1,3 @@
import { EventType, PeriodType } from "@prisma/client";
import dayjs from "@calcom/dayjs";
function isOutOfBounds(
time: dayjs.ConfigType,
{
periodType,
periodDays,
periodCountCalendarDays,
periodStartDate,
periodEndDate,
}: Pick<
EventType,
"periodType" | "periodDays" | "periodCountCalendarDays" | "periodStartDate" | "periodEndDate"
>
) {
const date = dayjs(time);
periodDays = periodDays || 0;
switch (periodType) {
case PeriodType.ROLLING: {
const periodRollingEndDay = periodCountCalendarDays
? dayjs().utcOffset(date.utcOffset()).add(periodDays, "days").endOf("day")
: dayjs().utcOffset(date.utcOffset()).businessDaysAdd(periodDays).endOf("day");
return date.endOf("day").isAfter(periodRollingEndDay);
}
case PeriodType.RANGE: {
const periodRangeStartDay = dayjs(periodStartDate).utcOffset(date.utcOffset()).endOf("day");
const periodRangeEndDay = dayjs(periodEndDate).utcOffset(date.utcOffset()).endOf("day");
return date.endOf("day").isBefore(periodRangeStartDay) || date.endOf("day").isAfter(periodRangeEndDay);
}
case PeriodType.UNLIMITED:
default:
return false;
}
}
export default isOutOfBounds;
/* Prefer import from `@calcom/lib/isOutOfBounds` */
export * from "@calcom/lib/isOutOfBounds";
export { default } from "@calcom/lib/isOutOfBounds";

View File

@ -2,10 +2,10 @@ import { I18n } from "next-i18next";
import { RRule } from "rrule";
import dayjs, { Dayjs } from "@calcom/dayjs";
import { inferQueryOutput } from "@calcom/trpc/react";
import { RecurringEvent } from "@calcom/types/Calendar";
import { detectBrowserTimeFormat } from "@lib/timeFormat";
import { inferQueryOutput } from "@lib/trpc";
import { parseZone } from "./parseZone";

View File

@ -1,8 +1,8 @@
import { PrismaClient } from "@prisma/client";
import { BASE_URL } from "@lib/config/constants";
import { TRPCError } from "@calcom/trpc/server";
import { TRPCError } from "@trpc/server";
import { BASE_URL } from "@lib/config/constants";
export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || "";
export const samlLoginUrl = BASE_URL;

View File

@ -1,114 +1,3 @@
import dayjs, { Dayjs } from "@calcom/dayjs";
import { getWorkingHours } from "./availability";
import { WorkingHours } from "./types/schedule";
export type GetSlots = {
inviteeDate: Dayjs;
frequency: number;
workingHours: WorkingHours[];
minimumBookingNotice: number;
eventLength: number;
};
export type WorkingHoursTimeFrame = { startTime: number; endTime: number };
const splitAvailableTime = (
startTimeMinutes: number,
endTimeMinutes: number,
frequency: number,
eventLength: number
): Array<WorkingHoursTimeFrame> => {
let initialTime = startTimeMinutes;
const finalizationTime = endTimeMinutes;
const result = [] as Array<WorkingHoursTimeFrame>;
while (initialTime < finalizationTime) {
const periodTime = initialTime + frequency;
const slotEndTime = initialTime + eventLength;
/*
check if the slot end time surpasses availability end time of the user
1 minute is added to round up the hour mark so that end of the slot is considered in the check instead of x9
eg: if finalization time is 11:59, slotEndTime is 12:00, we ideally want the slot to be available
*/
if (slotEndTime <= finalizationTime + 1) result.push({ startTime: initialTime, endTime: periodTime });
initialTime += frequency;
}
return result;
};
const getSlots = ({ inviteeDate, frequency, minimumBookingNotice, workingHours, eventLength }: GetSlots) => {
// current date in invitee tz
const startDate = dayjs().add(minimumBookingNotice, "minute");
const startOfDay = dayjs.utc().startOf("day");
const startOfInviteeDay = inviteeDate.startOf("day");
// checks if the start date is in the past
/**
* TODO: change "day" for "hour" to stop displaying 1 day before today
* This is displaying a day as available as sometimes difference between two dates is < 24 hrs.
* But when doing timezones an available day for an owner can be 2 days available in other users tz.
*
* */
if (inviteeDate.isBefore(startDate, "day")) {
return [];
}
const localWorkingHours = getWorkingHours(
{ utcOffset: -inviteeDate.utcOffset() },
workingHours.map((schedule) => ({
days: schedule.days,
startTime: startOfDay.add(schedule.startTime, "minute"),
endTime: startOfDay.add(schedule.endTime, "minute"),
}))
).filter((hours) => hours.days.includes(inviteeDate.day()));
const slots: Dayjs[] = [];
const slotsTimeFrameAvailable = [] as Array<WorkingHoursTimeFrame>;
// Here we split working hour in chunks for every frequency available that can fit in whole working hours
const computedLocalWorkingHours: WorkingHoursTimeFrame[] = [];
let tempComputeTimeFrame: WorkingHoursTimeFrame | undefined;
const computeLength = localWorkingHours.length - 1;
const makeTimeFrame = (item: typeof localWorkingHours[0]): WorkingHoursTimeFrame => ({
startTime: item.startTime,
endTime: item.endTime,
});
localWorkingHours.forEach((item, index) => {
if (!tempComputeTimeFrame) {
tempComputeTimeFrame = makeTimeFrame(item);
} else {
// please check the comment in splitAvailableTime func for the added 1 minute
if (tempComputeTimeFrame.endTime + 1 === item.startTime) {
// to deal with time that across the day, e.g. from 11:59 to to 12:01
tempComputeTimeFrame.endTime = item.endTime;
} else {
computedLocalWorkingHours.push(tempComputeTimeFrame);
tempComputeTimeFrame = makeTimeFrame(item);
}
}
if (index == computeLength) {
computedLocalWorkingHours.push(tempComputeTimeFrame);
}
});
computedLocalWorkingHours.forEach((item) => {
slotsTimeFrameAvailable.push(...splitAvailableTime(item.startTime, item.endTime, frequency, eventLength));
});
slotsTimeFrameAvailable.forEach((item) => {
const slot = startOfInviteeDay.add(item.startTime, "minute");
// Validating slot its not on the past
if (!slot.isBefore(startDate)) {
slots.push(slot);
}
});
const uniq = (a: Dayjs[]) => {
const seen: Record<string, boolean> = {};
return a.filter((item) => {
return seen.hasOwnProperty(item.format()) ? false : (seen[item.format()] = true);
});
};
return uniq(slots);
};
export default getSlots;
/** Prefer import from `@calcom/lib/slots` */
export * from "@calcom/lib/slots";
export { default } from "@calcom/lib/slots";

View File

@ -1,8 +1,17 @@
import { WebhookTriggerEvents } from "@prisma/client";
// this is exported as we can't use `WebhookTriggerEvents` in the frontend straight-off
export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
core: [
WebhookTriggerEvents.BOOKING_CANCELLED,
WebhookTriggerEvents.BOOKING_CREATED,
WebhookTriggerEvents.BOOKING_RESCHEDULED,
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"],
routing_forms: [WebhookTriggerEvents.FORM_SUBMITTED] as ["FORM_SUBMITTED"],
};
export const WEBHOOK_TRIGGER_EVENTS = [
WebhookTriggerEvents.BOOKING_CANCELLED,
WebhookTriggerEvents.BOOKING_CREATED,
WebhookTriggerEvents.BOOKING_RESCHEDULED,
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"];
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.core,
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.routing_forms,
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED", "FORM_SUBMITTED"];

View File

@ -34,10 +34,7 @@ const sendPayload = async (
bookingId?: number;
}
) => {
const { subscriberUrl, appId, payloadTemplate: template } = webhook;
if (!subscriberUrl || !data) {
throw new Error("Missing required elements to send webhook payload.");
}
const { appId, payloadTemplate: template } = webhook;
const contentType =
!template || jsonParse(template) ? "application/json" : "application/x-www-form-urlencoded";
@ -59,6 +56,33 @@ const sendPayload = async (
});
}
return _sendPayload(secretKey, triggerEvent, createdAt, webhook, body, contentType);
};
export const sendGenericWebhookPayload = async (
secretKey: string | null,
triggerEvent: string,
createdAt: string,
webhook: Pick<Webhook, "subscriberUrl" | "appId" | "payloadTemplate">,
data: Record<string, unknown>
) => {
const body = JSON.stringify(data);
return _sendPayload(secretKey, triggerEvent, createdAt, webhook, body, "application/json");
};
const _sendPayload = async (
secretKey: string | null,
triggerEvent: string,
createdAt: string,
webhook: Pick<Webhook, "subscriberUrl" | "appId" | "payloadTemplate">,
body: string,
contentType: "application/json" | "application/x-www-form-urlencoded"
) => {
const { subscriberUrl } = webhook;
if (!subscriberUrl || !body) {
throw new Error("Missing required elements to send webhook payload.");
}
const secretSignature = secretKey
? createHmac("sha256", secretKey).update(`${body}`).digest("hex")
: "no-secret-provided";

View File

@ -1,36 +1,10 @@
const path = require("path");
const i18nConfig = require("@calcom/config/next-i18next.config");
/** @type {import("next-i18next").UserConfig} */
const config = {
i18n: {
defaultLocale: "en",
locales: [
"en",
"fr",
"it",
"ru",
"es",
"de",
"pt",
"ro",
"nl",
"pt-BR",
"es-419",
"ko",
"ja",
"pl",
"ar",
"iw",
"zh-CN",
"zh-TW",
"cs",
"sr",
"sv",
"vi",
],
},
...i18nConfig,
localePath: path.resolve("./public/static/locales"),
reloadOnPrerender: process.env.NODE_ENV !== "production",
};
module.exports = config;

View File

@ -5,14 +5,15 @@ const withTM = require("next-transpile-modules")([
"@calcom/core",
"@calcom/dayjs",
"@calcom/ee",
"@calcom/lib",
"@calcom/prisma",
"@calcom/stripe",
"@calcom/ui",
"@calcom/emails",
"@calcom/embed-core",
"@calcom/embed-react",
"@calcom/embed-snippet",
"@calcom/lib",
"@calcom/prisma",
"@calcom/stripe",
"@calcom/trpc",
"@calcom/ui",
]);
const { i18n } = require("./next-i18next.config");

View File

@ -9,7 +9,8 @@
"clean": "rm -rf .turbo && rm -rf node_modules && rm -rf .next",
"dev": "next dev",
"dx": "yarn dev",
"test": "jest",
"test": "dotenv -e ./test/.env.test -- jest",
"db-setup-tests": "dotenv -e ./test/.env.test -- yarn workspace @calcom/prisma prisma migrate deploy",
"test-e2e": "cd ../.. && yarn playwright test --config=tests/config/playwright.config.ts --project=chromium",
"playwright-report": "playwright show-report playwright/reports/playwright-html-report",
"test-codegen": "yarn playwright codegen http://localhost:3000",
@ -27,8 +28,8 @@
},
"dependencies": {
"@boxyhq/saml-jackson": "0.3.6",
"@calcom/app-store": "*",
"@calcom/app-store-cli": "*",
"@calcom/app-store": "*",
"@calcom/core": "*",
"@calcom/dayjs": "*",
"@calcom/ee": "*",
@ -38,6 +39,7 @@
"@calcom/lib": "*",
"@calcom/prisma": "*",
"@calcom/stripe": "*",
"@calcom/trpc": "*",
"@calcom/tsconfig": "*",
"@calcom/ui": "*",
"@daily-co/daily-js": "^0.26.0",
@ -60,10 +62,6 @@
"@radix-ui/react-tooltip": "^0.1.0",
"@stripe/react-stripe-js": "^1.8.0",
"@stripe/stripe-js": "^1.29.0",
"@trpc/client": "^9.25.2",
"@trpc/next": "^9.25.2",
"@trpc/react": "^9.25.2",
"@trpc/server": "^9.25.2",
"@vercel/edge-functions-ui": "^0.2.1",
"@wojtekmaj/react-daterange-picker": "^3.3.1",
"accept-language-parser": "^1.5.0",
@ -89,6 +87,7 @@
"next-mdx-remote": "^4.0.3",
"next-seo": "^4.26.0",
"next-transpile-modules": "^9.0.0",
"nock": "^13.2.8",
"nodemailer": "^6.7.5",
"otplib": "^12.0.1",
"qrcode": "^1.5.0",

View File

@ -4,6 +4,14 @@ import Head from "next/head";
import superjson from "superjson";
import "@calcom/embed-core/src/embed-iframe";
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
import { httpLink } from "@calcom/trpc/client/links/httpLink";
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
import { splitLink } from "@calcom/trpc/client/links/splitLink";
import { withTRPC } from "@calcom/trpc/next";
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
import { Maybe } from "@calcom/trpc/server";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import LicenseRequired from "@ee/components/LicenseRequired";
import AppProviders, { AppProps } from "@lib/app-providers";
@ -12,15 +20,6 @@ import useTheme from "@lib/hooks/useTheme";
import I18nLanguageHandler from "@components/I18nLanguageHandler";
import type { AppRouter } from "@server/routers/_app";
import { httpBatchLink } from "@trpc/client/links/httpBatchLink";
import { httpLink } from "@trpc/client/links/httpLink";
import { loggerLink } from "@trpc/client/links/loggerLink";
import { splitLink } from "@trpc/client/links/splitLink";
import { withTRPC } from "@trpc/next";
import type { TRPCClientErrorLike } from "@trpc/react";
import { Maybe } from "@trpc/server";
import { ContractsProvider } from "../contexts/contractsContext";
import "../styles/fonts.css";
import "../styles/globals.css";

View File

@ -1,9 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "@lib/auth";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
import { createContext } from "@server/createContext";
import { viewerRouter } from "@server/routers/viewer";
import { getSession } from "@lib/auth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req });

View File

@ -8,8 +8,8 @@ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import logger from "@calcom/lib/logger";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { refund } from "@calcom/stripe/server";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
import { scheduleWorkflowReminders } from "@ee/lib/workflows/reminders/reminderScheduler";
import { getSession } from "@lib/auth";

View File

@ -18,20 +18,20 @@ import {
import { getLuckyUsers, isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import isOutOfBounds from "@calcom/lib/isOutOfBounds";
import logger from "@calcom/lib/logger";
import { defaultResponder } from "@calcom/lib/server";
import prisma, { userSelect } from "@calcom/prisma";
import { extendedBookingCreateBody } from "@calcom/prisma/zod-utils";
import { handlePayment } from "@calcom/stripe/server";
import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import { handlePayment } from "@ee/lib/stripe/server";
import { scheduleWorkflowReminders } from "@ee/lib/workflows/reminders/reminderScheduler";
import { HttpError } from "@lib/core/http/error";
import { ensureArray } from "@lib/ensureArray";
import { getEventName } from "@lib/event";
import isOutOfBounds from "@lib/isOutOfBounds";
import sendPayload from "@lib/webhooks/sendPayload";
import getSubscribers from "@lib/webhooks/subscriptions";

View File

@ -19,8 +19,8 @@ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { HttpError } from "@calcom/lib/http-error";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { refund } from "@calcom/stripe/server";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { refund } from "@ee/lib/stripe/server";
import { deleteScheduledEmailReminder } from "@ee/lib/workflows/reminders/emailReminderManager";
import { sendCancelledReminders } from "@ee/lib/workflows/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@ee/lib/workflows/reminders/smsReminderManager";

View File

@ -2,11 +2,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
import { getSession } from "next-auth/react";
import { defaultHandler } from "@calcom/lib/server";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import prisma from "@calcom/prisma";
import { userMetadata as zodUserMetadata } from "@calcom/prisma/zod-utils";
import { checkUsername } from "@lib/core/server/checkUsername";
export async function getHandler(req: NextApiRequest, res: NextApiResponse) {
const { intentUsername } = req.body;
// Check that user is authenticated

View File

@ -1,8 +1,9 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { getSession } from "@lib/auth";
import prisma from "@lib/prisma";
import { getTeamWithMembers } from "@lib/queries/teams";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const session = await getSession({ req: req });

View File

@ -1,9 +1,9 @@
/**
* This file contains tRPC's HTTP response handler
*/
import { createContext } from "@server/createContext";
import { appRouter } from "@server/routers/_app";
import * as trpcNext from "@trpc/server/adapters/next";
import * as trpcNext from "@calcom/trpc/server/adapters/next";
import { createContext } from "@calcom/trpc/server/createContext";
import { appRouter } from "@calcom/trpc/server/routers/_app";
export default trpcNext.createNextApiHandler({
router: appRouter,

View File

@ -1,6 +1,6 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { checkUsername } from "@lib/core/server/checkUsername";
import { checkUsername } from "@calcom/lib/server/checkUsername";
type Response = {
available: boolean;

View File

@ -6,6 +6,7 @@ import { InstallAppButton } from "@calcom/app-store/components";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { trpc } from "@calcom/trpc/react";
import type { App } from "@calcom/types/App";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
@ -14,7 +15,6 @@ import EmptyScreen from "@calcom/ui/EmptyScreen";
import { QueryCell } from "@lib/QueryCell";
import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error";
import { trpc } from "@lib/trpc";
import AppsShell from "@components/AppsShell";
import { List, ListItem, ListItemText, ListItemTitle } from "@components/List";

View File

@ -3,12 +3,12 @@ import { signIn } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect } from "react";
import { checkUsername } from "@calcom/lib/server/checkUsername";
import stripe from "@calcom/stripe/server";
import { getPremiumPlanPrice } from "@calcom/stripe/utils";
import { asStringOrNull } from "@lib/asStringOrNull";
import { getSession } from "@lib/auth";
import { checkUsername } from "@lib/core/server/checkUsername";
import prisma from "@lib/prisma";
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID, samlTenantProduct } from "@lib/saml";
import { inferSSRProps } from "@lib/types/inferSSRProps";

View File

@ -6,13 +6,13 @@ import { Controller, useForm } from "react-hook-form";
import { DEFAULT_SCHEDULE, availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import Button from "@calcom/ui/Button";
import Switch from "@calcom/ui/Switch";
import { Form } from "@calcom/ui/form/fields";
import { QueryCell } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import { inferQueryOutput, trpc } from "@lib/trpc";
import Shell from "@components/Shell";
import Schedule from "@components/availability/Schedule";
@ -141,7 +141,7 @@ export default function Availability() {
success={({ data }) => {
return (
<Shell
heading={<EditableHeading title={data.schedule.name} onChange={setName} />}
heading={<EditableHeading title={name || data.schedule.name} onChange={setName} />}
subtitle={data.schedule.availability.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, i18n.language)}

View File

@ -2,11 +2,11 @@ import { ClockIcon } from "@heroicons/react/outline";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import EmptyScreen from "@calcom/ui/EmptyScreen";
import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
import { inferQueryOutput, trpc } from "@lib/trpc";
import Shell from "@components/Shell";
import { NewScheduleButton } from "@components/availability/NewScheduleButton";

View File

@ -2,9 +2,9 @@ import { useEffect, useState } from "react";
import dayjs, { Dayjs } from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { QueryCell } from "@lib/QueryCell";
import { inferQueryOutput, trpc } from "@lib/trpc";
import Loader from "@components/Loader";
import Shell from "@components/Shell";

View File

@ -4,12 +4,12 @@ import { Fragment } from "react";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import EmptyScreen from "@calcom/ui/EmptyScreen";
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
import { inferQueryInput, inferQueryOutput, trpc } from "@lib/trpc";
import BookingsShell from "@components/BookingsShell";
import Shell from "@components/Shell";
@ -76,7 +76,7 @@ export default function Bookings() {
};
return (
<Shell heading={t("bookings")} subtitle={t("bookings_description")} customLoader={<SkeletonLoader />}>
<WipeMyCalActionButton trpc={trpc} bookingStatus={status} bookingsEmpty={isEmpty} />
<WipeMyCalActionButton bookingStatus={status} bookingsEmpty={isEmpty} />
<BookingsShell>
<div className="-mx-4 flex flex-col sm:mx-auto">
<div className="-my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">

View File

@ -37,6 +37,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import prisma from "@calcom/prisma";
import { StripeData } from "@calcom/stripe/server";
import { trpc } from "@calcom/trpc/react";
import { RecurringEvent } from "@calcom/types/Calendar";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
@ -52,7 +53,6 @@ import { HttpError } from "@lib/core/http/error";
import { isSuccessRedirectAvailable } from "@lib/isSuccessRedirectAvailable";
import { LocationObject, LocationType } from "@lib/location";
import { slugify } from "@lib/slugify";
import { trpc } from "@lib/trpc";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { ClientSuspense } from "@components/ClientSuspense";
@ -67,6 +67,7 @@ import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import RecurringEventController from "@components/eventtype/RecurringEventController";
import CustomInputTypeForm from "@components/pages/eventtypes/CustomInputTypeForm";
import Badge from "@components/ui/Badge";
import EditableHeading from "@components/ui/EditableHeading";
import InfoBadge from "@components/ui/InfoBadge";
import CheckboxField from "@components/ui/form/CheckboxField";
import CheckedSelect from "@components/ui/form/CheckedSelect";
@ -77,6 +78,7 @@ import * as RadioArea from "@components/ui/form/radio-area";
import WebhookListContainer from "@components/webhook/WebhookListContainer";
import { getTranslation } from "@server/lib/i18n";
import { TRPCClientError } from "@trpc/client";
interface Token {
name?: string;
@ -302,12 +304,13 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
if (err instanceof HttpError) {
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
} else if (err instanceof TRPCClientError) {
showToast(err.message, "error");
}
},
});
const connectedCalendarsQuery = trpc.useQuery(["viewer.connectedCalendars"]);
const [editIcon, setEditIcon] = useState(true);
const [showLocationModal, setShowLocationModal] = useState(false);
const [selectedLocation, setSelectedLocation] = useState<OptionTypeBase | undefined>(undefined);
const [selectedCustomInput, setSelectedCustomInput] = useState<EventTypeCustomInput | undefined>(undefined);
@ -315,7 +318,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
eventType.customInputs.sort((a, b) => a.id - b.id) || []
);
const [tokensList, setTokensList] = useState<Array<Token>>([]);
const defaultSeatsPro = 6;
const minSeats = 2;
@ -351,7 +353,6 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
async function deleteEventTypeHandler(event: React.MouseEvent<HTMLElement, MouseEvent>) {
event.preventDefault();
const payload = { id: eventType.id };
deleteMutation.mutate(payload);
}
@ -437,6 +438,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const formMethods = useForm<FormValues>({
defaultValues: {
title: eventType.title,
locations: eventType.locations || [],
recurringEvent: eventType.recurringEvent || null,
schedule: eventType.schedule?.id,
@ -844,37 +846,10 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
<Shell
title={t("event_type_title", { eventTypeTitle: eventType.title })}
heading={
<div className="group relative cursor-pointer" onClick={() => setEditIcon(false)}>
{editIcon ? (
<>
<h1
style={{ fontSize: 22, letterSpacing: "-0.0009em" }}
className="inline pl-0 text-gray-900 focus:text-black group-hover:text-gray-500">
{formMethods.getValues("title") && formMethods.getValues("title") !== ""
? formMethods.getValues("title")
: eventType.title}
</h1>
<PencilIcon className="ml-1 -mt-1 inline h-4 w-4 text-gray-700 group-hover:text-gray-500" />
</>
) : (
<div style={{ marginBottom: -11 }}>
<input
type="text"
autoFocus
style={{ top: -6, fontSize: 22 }}
required
className="relative h-10 w-full cursor-pointer border-none bg-transparent pl-0 text-gray-900 hover:text-gray-700 focus:text-black focus:outline-none focus:ring-0"
placeholder={t("quick_chat")}
{...formMethods.register("title")}
defaultValue={eventType.title}
onBlur={() => {
setEditIcon(true);
formMethods.getValues("title") === "" && formMethods.setValue("title", eventType.title);
}}
/>
</div>
)}
</div>
<EditableHeading
title={formMethods.watch("title")}
onChange={(value) => formMethods.setValue("title", value)}
/>
}
subtitle={eventType.description || ""}>
<ClientSuspense fallback={<Loader />}>
@ -1978,20 +1953,24 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-gray-700 hover:bg-gray-200 hover:text-gray-900"
eventTypeId={eventType.id}
/>
<Dialog>
<DialogTrigger className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-red-500 hover:bg-gray-200">
<TrashIcon className="h-4 w-4 text-red-500 ltr:mr-2 rtl:ml-2" />
{t("delete")}
</DialogTrigger>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_event_type")}
confirmBtnText={t("confirm_delete_event_type")}
onConfirm={deleteEventTypeHandler}>
{t("delete_event_type_description")}
</ConfirmationDialogContent>
</Dialog>
{/* This will only show if the user is not a member (ADMIN,OWNER) and if there is no current membership
- meaning you are within an eventtype that does not belong to a team */}
{(props.currentUserMembership?.role !== "MEMBER" || !props.currentUserMembership) && (
<Dialog>
<DialogTrigger className="text-md flex items-center rounded-sm px-2 py-1 text-sm font-medium text-red-500 hover:bg-gray-200">
<TrashIcon className="h-4 w-4 text-red-500 ltr:mr-2 rtl:ml-2" />
{t("delete")}
</DialogTrigger>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
variety="danger"
title={t("delete_event_type")}
confirmBtnText={t("confirm_delete_event_type")}
onConfirm={deleteEventTypeHandler}>
{t("delete_event_type_description")}
</ConfirmationDialogContent>
</Dialog>
)}
</div>
</div>
</div>
@ -2281,6 +2260,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
})
: [];
// Find the current users memebership so we can check role to enable/disable deletion.
// Sets to null if no membership is found - this must mean we are in a none team event type
const currentUserMembership =
eventTypeObject.team?.members.find((el) => el.user.id === session.user.id) ?? null;
return {
props: {
session,
@ -2292,6 +2276,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
hasPaymentIntegration,
hasGiphyIntegration,
currency,
currentUserMembership,
},
};
};

View File

@ -22,6 +22,7 @@ import React, { Fragment, useEffect, useState } from "react";
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert";
import { Dialog } from "@calcom/ui/Dialog";
@ -37,7 +38,6 @@ import { Tooltip } from "@calcom/ui/Tooltip";
import { withQuery } from "@lib/QueryCell";
import classNames from "@lib/classNames";
import { HttpError } from "@lib/core/http/error";
import { inferQueryOutput, trpc } from "@lib/trpc";
import { EmbedButton, EmbedDialog } from "@components/Embed";
import Shell from "@components/Shell";
@ -49,6 +49,8 @@ import Avatar from "@components/ui/Avatar";
import AvatarGroup from "@components/ui/AvatarGroup";
import Badge from "@components/ui/Badge";
import { TRPCClientError } from "@trpc/react";
type EventTypeGroups = inferQueryOutput<"viewer.eventTypes">["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
interface EventTypeListHeadingProps {
@ -193,6 +195,8 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const message = `${err.statusCode}: ${err.message}`;
showToast(message, "error");
setDeleteDialogOpen(false);
} else if (err instanceof TRPCClientError) {
showToast(err.message, "error");
}
},
});
@ -332,19 +336,22 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
/>
</DropdownMenuItem>
<DropdownMenuSeparator className="h-px bg-gray-200" />
<DropdownMenuItem>
<Button
onClick={() => {
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
}}
color="warn"
size="sm"
StartIcon={TrashIcon}
className="w-full rounded-none">
{t("delete") as string}
</Button>
</DropdownMenuItem>
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
<DropdownMenuItem>
<Button
onClick={() => {
setDeleteDialogOpen(true);
setDeleteDialogTypeId(type.id);
}}
color="warn"
size="sm"
StartIcon={TrashIcon}
className="w-full rounded-none">
{t("delete") as string}
</Button>
</DropdownMenuItem>
)}
</DropdownMenuContent>
</Dropdown>
</div>

View File

@ -17,6 +17,7 @@ import { getCalendarCredentials, getConnectedCalendars } from "@calcom/core/Cale
import dayjs from "@calcom/dayjs";
import { DOCS_URL } from "@calcom/lib/constants";
import { fetchUsername } from "@calcom/lib/fetchUsername";
import { trpc } from "@calcom/trpc/react";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Form } from "@calcom/ui/form/fields";
@ -26,7 +27,6 @@ import { DEFAULT_SCHEDULE } from "@lib/availability";
import { useLocale } from "@lib/hooks/useLocale";
import prisma from "@lib/prisma";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry";
import { trpc } from "@lib/trpc";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { Schedule as ScheduleType } from "@lib/types/schedule";

View File

@ -1,4 +1,5 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import ApiKeyListContainer from "@ee/components/apiKeys/ApiKeyListContainer";
import SettingsShell from "@components/SettingsShell";
@ -6,10 +7,23 @@ import WebhookListContainer from "@components/webhook/WebhookListContainer";
export default function Settings() {
const { t } = useLocale();
const { data: routingForms } = trpc.useQuery([
"viewer.appById",
{
appId: "routing_forms",
},
]);
return (
<SettingsShell heading={t("developer")} subtitle={t("manage_developer_settings")}>
<WebhookListContainer title={t("webhooks")} subtitle={t("receive_cal_meeting_data")} />
<WebhookListContainer title="Event Webhooks" subtitle={t("receive_cal_meeting_data")} />
{routingForms && (
<WebhookListContainer
appId="routing_forms"
title="Routing Webhooks"
subtitle="Receive Routing Form responses at a specified URL, in real-time, when a Routing Form is submitted"
/>
)}
<ApiKeyListContainer />
</SettingsShell>
);

View File

@ -8,6 +8,9 @@ import TimezoneSelect, { ITimezone } from "react-timezone-select";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Alert } from "@calcom/ui/Alert";
import Button from "@calcom/ui/Button";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
@ -18,7 +21,6 @@ import { getSession } from "@lib/auth";
import { nameOfDay } from "@lib/core/i18n/weekday";
import { isBrandingHidden } from "@lib/isBrandingHidden";
import prisma from "@lib/prisma";
import { trpc } from "@lib/trpc";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import ImageUploader from "@components/ImageUploader";
@ -31,9 +33,6 @@ import { UsernameAvailability } from "@components/ui/UsernameAvailability";
import ColorPicker from "@components/ui/colorpicker";
import Select from "@components/ui/form/Select";
import { AppRouter } from "@server/routers/_app";
import { TRPCClientErrorLike } from "@trpc/client";
import { UpgradeToProDialog } from "../../components/UpgradeToProDialog";
type Props = inferSSRProps<typeof getServerSideProps>;

View File

@ -2,10 +2,10 @@ import { IdentityProvider } from "@prisma/client";
import React from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import SAMLConfiguration from "@ee/components/saml/Configuration";
import { identityProviderNameMap } from "@lib/auth";
import { trpc } from "@lib/trpc";
import SettingsShell from "@components/SettingsShell";
import ChangePasswordSection from "@components/security/ChangePasswordSection";

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