diff --git a/.env.example b/.env.example index d2da9e421c..f45725ad14 100644 --- a/.env.example +++ b/.env.example @@ -127,3 +127,6 @@ EMAIL_SERVER_PASSWORD='' ## @see https://support.google.com/accounts/answer/185833 # EMAIL_SERVER_PASSWORD='' # ********************************************************************************************************** + +# Set the following value to true if you wish to enable Team Impersonation +NEXT_PUBLIC_TEAM_IMPERSONATION=false diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 2f10275dfe..ff514935a7 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -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 }}" diff --git a/apps/console b/apps/console index 8a796ab11d..a26db083fa 160000 --- a/apps/console +++ b/apps/console @@ -1 +1 @@ -Subproject commit 8a796ab11dd6663be5e582e7bac49f0aabd92dc3 +Subproject commit a26db083faaa79a40f96dddac888ba2c2bea921e diff --git a/apps/web/components/AdditionalCalendarSelector.tsx b/apps/web/components/AdditionalCalendarSelector.tsx index 192aff0df2..dbeab7cb8e 100644 --- a/apps/web/components/AdditionalCalendarSelector.tsx +++ b/apps/web/components/AdditionalCalendarSelector.tsx @@ -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; diff --git a/apps/web/components/App.tsx b/apps/web/components/App.tsx index 5704f2df57..98f1b7f25f 100644 --- a/apps/web/components/App.tsx +++ b/apps/web/components/App.tsx @@ -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"; diff --git a/apps/web/components/DestinationCalendarSelector.tsx b/apps/web/components/DestinationCalendarSelector.tsx index 25f4a7f5ac..d459a03c93 100644 --- a/apps/web/components/DestinationCalendarSelector.tsx +++ b/apps/web/components/DestinationCalendarSelector.tsx @@ -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; diff --git a/apps/web/components/Embed.tsx b/apps/web/components/Embed.tsx index 736931db18..cc4c8fdc3d 100644 --- a/apps/web/components/Embed.tsx +++ b/apps/web/components/Embed.tsx @@ -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"; diff --git a/apps/web/components/I18nLanguageHandler.tsx b/apps/web/components/I18nLanguageHandler.tsx index 1a78b6557e..c4c7d2070e 100644 --- a/apps/web/components/I18nLanguageHandler.tsx +++ b/apps/web/components/I18nLanguageHandler.tsx @@ -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"], { diff --git a/apps/web/components/PencilEdit.tsx b/apps/web/components/PencilEdit.tsx deleted file mode 100644 index 0d958e3480..0000000000 --- a/apps/web/components/PencilEdit.tsx +++ /dev/null @@ -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 ( -
- {editIcon ? ( - <> -

- {value} -

- {!readOnly ? ( - - ) : null} - - ) : ( -
- { - setEditIcon(true); - onChange(e.target.value); - }} - /> -
- )} -
- ); -} diff --git a/apps/web/components/Shell.tsx b/apps/web/components/Shell.tsx index d6e2b517ee..6eee53d45c 100644 --- a/apps/web/components/Shell.tsx +++ b/apps/web/components/Shell.tsx @@ -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 }) { { - 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"> diff --git a/apps/web/components/apps/AppCard.tsx b/apps/web/components/apps/AppCard.tsx index c6a6ee17f4..6397b9e46b 100644 --- a/apps/web/components/apps/AppCard.tsx +++ b/apps/web/components/apps/AppCard.tsx @@ -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 { diff --git a/apps/web/components/auth/SAMLLogin.tsx b/apps/web/components/auth/SAMLLogin.tsx index 76f7c75c8c..62ec48ef0f 100644 --- a/apps/web/components/auth/SAMLLogin.tsx +++ b/apps/web/components/auth/SAMLLogin.tsx @@ -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; diff --git a/apps/web/components/availability/NewScheduleButton.tsx b/apps/web/components/availability/NewScheduleButton.tsx index 1d1fc7ae30..b0f591b466 100644 --- a/apps/web/components/availability/NewScheduleButton.tsx +++ b/apps/web/components/availability/NewScheduleButton.tsx @@ -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(); diff --git a/apps/web/components/availability/Schedule.tsx b/apps/web/components/availability/Schedule.tsx index 22a60acb19..01df625fc6 100644 --- a/apps/web/components/availability/Schedule.tsx +++ b/apps/web/components/availability/Schedule.tsx @@ -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 = ({ {index === 0 && (
- +
+ + + + ); +}; + +export default DisableTeamImpersonation; diff --git a/apps/web/components/team/MemberChangeRoleModal.tsx b/apps/web/components/team/MemberChangeRoleModal.tsx index 263cd3d968..642675bc7c 100644 --- a/apps/web/components/team/MemberChangeRoleModal.tsx +++ b/apps/web/components/team/MemberChangeRoleModal.tsx @@ -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"; diff --git a/apps/web/components/team/MemberInvitationModal.tsx b/apps/web/components/team/MemberInvitationModal.tsx index 7d7495d398..2ead04d2d9 100644 --- a/apps/web/components/team/MemberInvitationModal.tsx +++ b/apps/web/components/team/MemberInvitationModal.tsx @@ -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"; diff --git a/apps/web/components/team/MemberList.tsx b/apps/web/components/team/MemberList.tsx index 0d41d4d693..24cf37ebe7 100644 --- a/apps/web/components/team/MemberList.tsx +++ b/apps/web/components/team/MemberList.tsx @@ -1,4 +1,4 @@ -import { inferQueryOutput } from "@lib/trpc"; +import { inferQueryOutput } from "@calcom/trpc/react"; import MemberListItem from "./MemberListItem"; diff --git a/apps/web/components/team/MemberListItem.tsx b/apps/web/components/team/MemberListItem.tsx index f77e022228..18ee2496e8 100644 --- a/apps/web/components/team/MemberListItem.tsx +++ b/apps/web/components/team/MemberListItem.tsx @@ -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) {
+ {/* 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" && ( + <> + + + + + + )} @@ -185,6 +205,39 @@ export default function MemberListItem(props: Props) { onExit={() => setShowChangeMemberRoleModal(false)} /> )} + {showImpersonateModal && props.member.username && ( + setShowImpersonateModal(false)}> + <> +
+
+ +
+
+
{ + e.preventDefault(); + await signIn("impersonation-auth", { + username: props.member.username, + teamId: props.team.id, + }); + }}> +

+ {t("impersonate_user_tip")} +

+
+ + +
+
+ +
+ )} {showTeamAvailabilityModal && ( void; diff --git a/apps/web/components/team/TeamList.tsx b/apps/web/components/team/TeamList.tsx index f817770042..fdb9466f72 100644 --- a/apps/web/components/team/TeamList.tsx +++ b/apps/web/components/team/TeamList.tsx @@ -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"; diff --git a/apps/web/components/team/TeamListItem.tsx b/apps/web/components/team/TeamListItem.tsx index 57482bffe0..a6901fa40b 100644 --- a/apps/web/components/team/TeamListItem.tsx +++ b/apps/web/components/team/TeamListItem.tsx @@ -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"; diff --git a/apps/web/components/team/TeamSettings.tsx b/apps/web/components/team/TeamSettings.tsx index 16f81c94ee..d1134e7f62 100644 --- a/apps/web/components/team/TeamSettings.tsx +++ b/apps/web/components/team/TeamSettings.tsx @@ -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"; diff --git a/apps/web/components/team/TeamSettingsRightSidebar.tsx b/apps/web/components/team/TeamSettingsRightSidebar.tsx index e4c857f595..420c153594 100644 --- a/apps/web/components/team/TeamSettingsRightSidebar.tsx +++ b/apps/web/components/team/TeamSettingsRightSidebar.tsx @@ -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"; diff --git a/apps/web/components/team/UpgradeToFlexibleProModal.tsx b/apps/web/components/team/UpgradeToFlexibleProModal.tsx index 19ec8d3925..34e19e5714 100644 --- a/apps/web/components/team/UpgradeToFlexibleProModal.tsx +++ b/apps/web/components/team/UpgradeToFlexibleProModal.tsx @@ -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; diff --git a/apps/web/components/ui/Avatar.tsx b/apps/web/components/ui/Avatar.tsx index d20056d095..44373297f7 100644 --- a/apps/web/components/ui/Avatar.tsx +++ b/apps/web/components/ui/Avatar.tsx @@ -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; diff --git a/apps/web/components/ui/EditableHeading.tsx b/apps/web/components/ui/EditableHeading.tsx index 3e3c5f9457..5d8d65c524 100644 --- a/apps/web/components/ui/EditableHeading.tsx +++ b/apps/web/components/ui/EditableHeading.tsx @@ -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 ( -
setEditIcon(false)}> - {editIcon ? ( +
+ {!isEditing ? ( <>

+ className="inline pl-0 normal-case text-gray-900 focus:text-black group-hover:text-gray-500"> {title}

- + {!readOnly ? ( + + ) : null} ) : (
@@ -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); + }} />
)} diff --git a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx index 8f86a9465e..685c662c6f 100644 --- a/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/PremiumTextfield.tsx @@ -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) => {
-

- {t("current")} {t("username")} -

+

{t("current_username")}

{currentUsername}

- {t("new")} {t("username")} + {t("new_username")}

{inputUsernameValue}

diff --git a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx index ce7c337ba9..3971f92f56 100644 --- a/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx +++ b/apps/web/components/ui/UsernameAvailability/UsernameTextfield.tsx @@ -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) => {
)}
-
+
@@ -171,16 +169,14 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
-

- {t("current")} {t("username").toLocaleLowerCase()} -

+

{t("current_username")}

{currentUsername}

- {t("new")} {t("username").toLocaleLowerCase()} + {t("new_username")}

{inputUsernameValue}

diff --git a/apps/web/components/webhook/WebhookDialogForm.tsx b/apps/web/components/webhook/WebhookDialogForm.tsx index 9fa9487952..6773e9b649 100644 --- a/apps/web/components/webhook/WebhookDialogForm.tsx +++ b/apps/web/components/webhook/WebhookDialogForm.tsx @@ -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: {
{t("event_triggers")} - {WEBHOOK_TRIGGER_EVENTS.map((key) => ( + {triggers.map((key) => ( (null); @@ -66,6 +70,7 @@ export default function WebhookListContainer(props: WebhookListContainerType) { !isOpen && setNewWebhookModal(false)}> setNewWebhookModal(false)} /> @@ -76,6 +81,7 @@ export default function WebhookListContainer(props: WebhookListContainerType) { {editing && ( setEditModalOpen(false)} diff --git a/apps/web/components/webhook/WebhookListItem.tsx b/apps/web/components/webhook/WebhookListItem.tsx index 4f16386656..fdf77a9265 100644 --- a/apps/web/components/webhook/WebhookListItem.tsx +++ b/apps/web/components/webhook/WebhookListItem.tsx @@ -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"; diff --git a/apps/web/components/webhook/WebhookTestDisclosure.tsx b/apps/web/components/webhook/WebhookTestDisclosure.tsx index ffd94db8e9..7fd419916b 100644 --- a/apps/web/components/webhook/WebhookTestDisclosure.tsx +++ b/apps/web/components/webhook/WebhookTestDisclosure.tsx @@ -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" }); diff --git a/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx b/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx index 370d8e7c7a..9a6b0b6ab0 100644 --- a/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx +++ b/apps/web/ee/components/apiKeys/ApiKeyDialogForm.tsx @@ -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"; diff --git a/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx b/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx index 47fedac8da..219eaca433 100644 --- a/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx +++ b/apps/web/ee/components/apiKeys/ApiKeyListContainer.tsx @@ -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"; diff --git a/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx b/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx index 81ac3ec46e..c1c6a1c6b2 100644 --- a/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx +++ b/apps/web/ee/components/apiKeys/ApiKeyListItem.tsx @@ -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"; diff --git a/apps/web/ee/components/saml/Configuration.tsx b/apps/web/ee/components/saml/Configuration.tsx index 7778b647da..d2e4fe34d3 100644 --- a/apps/web/ee/components/saml/Configuration.tsx +++ b/apps/web/ee/components/saml/Configuration.tsx @@ -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"; diff --git a/apps/web/ee/components/support/HelpMenuItem.tsx b/apps/web/ee/components/support/HelpMenuItem.tsx index 757b71db2e..25f0231034 100644 --- a/apps/web/ee/components/support/HelpMenuItem.tsx +++ b/apps/web/ee/components/support/HelpMenuItem.tsx @@ -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"; diff --git a/apps/web/ee/components/team/availability/TeamAvailabilityModal.tsx b/apps/web/ee/components/team/availability/TeamAvailabilityModal.tsx index 35308514a5..70cdc4bb7f 100644 --- a/apps/web/ee/components/team/availability/TeamAvailabilityModal.tsx +++ b/apps/web/ee/components/team/availability/TeamAvailabilityModal.tsx @@ -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"; diff --git a/apps/web/ee/components/team/availability/TeamAvailabilityScreen.tsx b/apps/web/ee/components/team/availability/TeamAvailabilityScreen.tsx index 55ae53e831..763a9da339 100644 --- a/apps/web/ee/components/team/availability/TeamAvailabilityScreen.tsx +++ b/apps/web/ee/components/team/availability/TeamAvailabilityScreen.tsx @@ -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"; diff --git a/apps/web/ee/components/team/availability/TeamAvailabilityTimes.tsx b/apps/web/ee/components/team/availability/TeamAvailabilityTimes.tsx index f89aff6718..601ed1e0c6 100644 --- a/apps/web/ee/components/team/availability/TeamAvailabilityTimes.tsx +++ b/apps/web/ee/components/team/availability/TeamAvailabilityTimes.tsx @@ -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"; diff --git a/apps/web/ee/components/workflows/NewWorkflowButton.tsx b/apps/web/ee/components/workflows/NewWorkflowButton.tsx index b62d7355bb..5840e96c05 100644 --- a/apps/web/ee/components/workflows/NewWorkflowButton.tsx +++ b/apps/web/ee/components/workflows/NewWorkflowButton.tsx @@ -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"; diff --git a/apps/web/ee/components/workflows/WorkflowDetailsPage.tsx b/apps/web/ee/components/workflows/WorkflowDetailsPage.tsx index 4641815922..b936a201ef 100644 --- a/apps/web/ee/components/workflows/WorkflowDetailsPage.tsx +++ b/apps/web/ee/components/workflows/WorkflowDetailsPage.tsx @@ -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; + form: UseFormReturn; workflowId: number; selectedEventTypes: Option[]; setSelectedEventTypes: Dispatch>; @@ -29,30 +28,31 @@ export default function WorkflowDetailsPage(props: Props) { const router = useRouter(); const utils = trpc.useContext(); - const [evenTypeOptions, setEventTypeOptions] = useState([]); 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 ( ; + form: UseFormReturn; reload?: boolean; setReload?: Dispatch>; editCounter: number; diff --git a/apps/web/ee/lib/impersonation/ImpersonationProvider.ts b/apps/web/ee/lib/impersonation/ImpersonationProvider.ts index 6d53b683f1..901a274a4d 100644 --- a/apps/web/ee/lib/impersonation/ImpersonationProvider.ts +++ b/apps/web/ee/lib/impersonation/ImpersonationProvider.ts @@ -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, + 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); }, }); diff --git a/apps/web/ee/lib/stripe/server.ts b/apps/web/ee/lib/stripe/server.ts deleted file mode 100644 index 6796946bb0..0000000000 --- a/apps/web/ee/lib/stripe/server.ts +++ /dev/null @@ -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 }, - }); -} diff --git a/apps/web/ee/lib/workflows/reminders/emailReminderManager.ts b/apps/web/ee/lib/workflows/reminders/emailReminderManager.ts index 1d15d6254d..18b434e443 100644 --- a/apps/web/ee/lib/workflows/reminders/emailReminderManager.ts +++ b/apps/web/ee/lib/workflows/reminders/emailReminderManager.ts @@ -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: `${emailBody}`, + }, + }; + 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(), }); diff --git a/apps/web/ee/lib/workflows/reminders/smsReminderManager.ts b/apps/web/ee/lib/workflows/reminders/smsReminderManager.ts index 78473f2ea4..62745da685 100644 --- a/apps/web/ee/lib/workflows/reminders/smsReminderManager.ts +++ b/apps/web/ee/lib/workflows/reminders/smsReminderManager.ts @@ -4,7 +4,7 @@ import { WorkflowTemplates, WorkflowActions, WorkflowMethods, -} from "@prisma/client/"; +} from "@prisma/client"; import dayjs from "@calcom/dayjs"; import prisma from "@calcom/prisma"; diff --git a/apps/web/ee/lib/workflows/reminders/templates/emailReminderTemplate.ts b/apps/web/ee/lib/workflows/reminders/templates/emailReminderTemplate.ts index 60368f797a..f3c18b6d3b 100644 --- a/apps/web/ee/lib/workflows/reminders/templates/emailReminderTemplate.ts +++ b/apps/web/ee/lib/workflows/reminders/templates/emailReminderTemplate.ts @@ -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 = `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}.`; - return emailContent; + const emailBody = { text: templateBodyText, html: templateBodyHtml }; + + return { emailSubject, emailBody }; }; export default emailReminderTemplate; diff --git a/apps/web/ee/pages/api/cron/workflows/scheduleEmailReminders.ts b/apps/web/ee/pages/api/cron/workflows/scheduleEmailReminders.ts index 4dabde6756..fbdafb64e2 100644 --- a/apps/web/ee/pages/api/cron/workflows/scheduleEmailReminders.ts +++ b/apps/web/ee/pages/api/cron/workflows/scheduleEmailReminders.ts @@ -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: `${reminder.workflowStep.reminderBody || ""}`, + }, + }; + 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(), }); diff --git a/apps/web/ee/pages/settings/teams/[id]/availability.tsx b/apps/web/ee/pages/settings/teams/[id]/availability.tsx index 6b280f7337..13de941875 100644 --- a/apps/web/ee/pages/settings/teams/[id]/availability.tsx +++ b/apps/web/ee/pages/settings/teams/[id]/availability.tsx @@ -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"; diff --git a/apps/web/ee/pages/workflows/[workflow].tsx b/apps/web/ee/pages/workflows/[workflow].tsx index 3365b37de2..84adc3c819 100644 --- a/apps/web/ee/pages/workflows/[workflow].tsx +++ b/apps/web/ee/pages/workflows/[workflow].tsx @@ -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([]); 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({ 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 ; - } - return ( setEditIcon(false)}> {editIcon ? ( <> @@ -160,15 +155,21 @@ function WorkflowPage() { ) : ( <> - {isAllDataLoaded ? ( - + {!isError ? ( + <> + {isAllDataLoaded ? ( + + ) : ( + + )} + ) : ( - + )} )} diff --git a/apps/web/ee/pages/workflows/index.tsx b/apps/web/ee/pages/workflows/index.tsx index d2fc642482..c3e075923b 100644 --- a/apps/web/ee/pages/workflows/index.tsx +++ b/apps/web/ee/pages/workflows/index.tsx @@ -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() { : <>}> + CTA={session.data?.hasValidLicense && !isFreeUser ? : <>}> {isLoading ? ( diff --git a/apps/web/jest.config.ts b/apps/web/jest.config.ts index 2f719b82db..92fe6767d7 100644 --- a/apps/web/jest.config.ts +++ b/apps/web/jest.config.ts @@ -3,7 +3,7 @@ import type { Config } from "@jest/types"; const config: Config.InitialOptions = { verbose: true, roots: [""], - testMatch: ["**/tests/**/*.+(ts|tsx|js)", "**/?(*.)+(spec|test).+(ts|tsx|js)"], + testMatch: ["**/test/lib/**/*.(spec|test).(ts|tsx|js)"], testPathIgnorePatterns: ["/.next", "/playwright/"], transform: { "^.+\\.(js|jsx|ts|tsx)$": ["babel-jest", { presets: ["next/babel"] }], diff --git a/apps/web/lib/QueryCell.tsx b/apps/web/lib/QueryCell.tsx index 9918d2b3c3..b1ee6fa588 100644 --- a/apps/web/lib/QueryCell.tsx +++ b/apps/web/lib/QueryCell.tsx @@ -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; diff --git a/apps/web/lib/app-providers.tsx b/apps/web/lib/app-providers.tsx index 54b899002f..d5cf8226d8 100644 --- a/apps/web/lib/app-providers.tsx +++ b/apps/web/lib/app-providers.tsx @@ -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(({ children }) => ( <>{children} )); diff --git a/apps/web/lib/core/i18n/i18n.utils.ts b/apps/web/lib/core/i18n/i18n.utils.ts index 1ba93e3d91..9899a5654e 100644 --- a/apps/web/lib/core/i18n/i18n.utils.ts +++ b/apps/web/lib/core/i18n/i18n.utils.ts @@ -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 { diff --git a/apps/web/lib/hooks/useMeQuery.ts b/apps/web/lib/hooks/useMeQuery.ts index a8d0a051ce..0eb0bb77fd 100644 --- a/apps/web/lib/hooks/useMeQuery.ts +++ b/apps/web/lib/hooks/useMeQuery.ts @@ -1,4 +1,4 @@ -import { trpc } from "../trpc"; +import { trpc } from "@calcom/trpc/react"; export function useMeQuery() { const meQuery = trpc.useQuery(["viewer.me"], { diff --git a/apps/web/lib/hooks/useTheme.tsx b/apps/web/lib/hooks/useTheme.tsx index 38dc9f504b..d7891ba41d 100644 --- a/apps/web/lib/hooks/useTheme.tsx +++ b/apps/web/lib/hooks/useTheme.tsx @@ -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 diff --git a/apps/web/lib/isOutOfBounds.tsx b/apps/web/lib/isOutOfBounds.tsx index 1a115f737d..d4d3bacbbc 100644 --- a/apps/web/lib/isOutOfBounds.tsx +++ b/apps/web/lib/isOutOfBounds.tsx @@ -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"; diff --git a/apps/web/lib/parseDate.ts b/apps/web/lib/parseDate.ts index 1b07301e9c..4d068fe427 100644 --- a/apps/web/lib/parseDate.ts +++ b/apps/web/lib/parseDate.ts @@ -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"; diff --git a/apps/web/lib/saml.ts b/apps/web/lib/saml.ts index 8dda87d125..6af24b61c9 100644 --- a/apps/web/lib/saml.ts +++ b/apps/web/lib/saml.ts @@ -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; diff --git a/apps/web/lib/slots.ts b/apps/web/lib/slots.ts index 1e48700249..82c1fff3e7 100644 --- a/apps/web/lib/slots.ts +++ b/apps/web/lib/slots.ts @@ -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 => { - let initialTime = startTimeMinutes; - const finalizationTime = endTimeMinutes; - const result = [] as Array; - 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; - // 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 = {}; - 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"; diff --git a/apps/web/lib/webhooks/constants.ts b/apps/web/lib/webhooks/constants.ts index ddd5a44507..ee02568bce 100644 --- a/apps/web/lib/webhooks/constants.ts +++ b/apps/web/lib/webhooks/constants.ts @@ -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"]; diff --git a/apps/web/lib/webhooks/sendPayload.tsx b/apps/web/lib/webhooks/sendPayload.tsx index 4bfa6b440c..5eb1d019cf 100644 --- a/apps/web/lib/webhooks/sendPayload.tsx +++ b/apps/web/lib/webhooks/sendPayload.tsx @@ -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, + data: Record +) => { + 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, + 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"; diff --git a/apps/web/next-i18next.config.js b/apps/web/next-i18next.config.js index 923695bde3..0d55e2dabb 100644 --- a/apps/web/next-i18next.config.js +++ b/apps/web/next-i18next.config.js @@ -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; diff --git a/apps/web/next.config.js b/apps/web/next.config.js index 9d16c1fc8b..62aa8d0781 100644 --- a/apps/web/next.config.js +++ b/apps/web/next.config.js @@ -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"); diff --git a/apps/web/package.json b/apps/web/package.json index b54cbbf5e6..b84bb6d488 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/apps/web/pages/_app.tsx b/apps/web/pages/_app.tsx index 57db0b2baa..55126806d4 100644 --- a/apps/web/pages/_app.tsx +++ b/apps/web/pages/_app.tsx @@ -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"; diff --git a/apps/web/pages/api/availability/eventtype.ts b/apps/web/pages/api/availability/eventtype.ts index be214855d5..e5721a5258 100644 --- a/apps/web/pages/api/availability/eventtype.ts +++ b/apps/web/pages/api/availability/eventtype.ts @@ -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 }); diff --git a/apps/web/pages/api/book/confirm.ts b/apps/web/pages/api/book/confirm.ts index 76b0dd0529..77fd3ff925 100644 --- a/apps/web/pages/api/book/confirm.ts +++ b/apps/web/pages/api/book/confirm.ts @@ -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"; diff --git a/apps/web/pages/api/book/event.ts b/apps/web/pages/api/book/event.ts index 76ba1a5bde..2ea3373d59 100644 --- a/apps/web/pages/api/book/event.ts +++ b/apps/web/pages/api/book/event.ts @@ -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"; diff --git a/apps/web/pages/api/cancel.ts b/apps/web/pages/api/cancel.ts index 582d2d5f8e..1ab17eff59 100644 --- a/apps/web/pages/api/cancel.ts +++ b/apps/web/pages/api/cancel.ts @@ -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"; diff --git a/apps/web/pages/api/intent-username/index.ts b/apps/web/pages/api/intent-username/index.ts index d43cbf874f..c74002830b 100644 --- a/apps/web/pages/api/intent-username/index.ts +++ b/apps/web/pages/api/intent-username/index.ts @@ -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 diff --git a/apps/web/pages/api/teams/[team]/index.ts b/apps/web/pages/api/teams/[team]/index.ts index 881540b28a..e576069c13 100644 --- a/apps/web/pages/api/teams/[team]/index.ts +++ b/apps/web/pages/api/teams/[team]/index.ts @@ -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 }); diff --git a/apps/web/pages/api/trpc/[trpc].ts b/apps/web/pages/api/trpc/[trpc].ts index da0ab1feda..7204fc2a30 100644 --- a/apps/web/pages/api/trpc/[trpc].ts +++ b/apps/web/pages/api/trpc/[trpc].ts @@ -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, diff --git a/apps/web/pages/api/username.ts b/apps/web/pages/api/username.ts index 57fddb64d0..9251aada56 100644 --- a/apps/web/pages/api/username.ts +++ b/apps/web/pages/api/username.ts @@ -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; diff --git a/apps/web/pages/apps/installed.tsx b/apps/web/pages/apps/installed.tsx index f0b46d02cc..fc39738c71 100644 --- a/apps/web/pages/apps/installed.tsx +++ b/apps/web/pages/apps/installed.tsx @@ -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"; diff --git a/apps/web/pages/auth/sso/[provider].tsx b/apps/web/pages/auth/sso/[provider].tsx index 9b0fc6fd12..707e640ec0 100644 --- a/apps/web/pages/auth/sso/[provider].tsx +++ b/apps/web/pages/auth/sso/[provider].tsx @@ -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"; diff --git a/apps/web/pages/availability/[schedule].tsx b/apps/web/pages/availability/[schedule].tsx index 224798a858..66e1e80e7c 100644 --- a/apps/web/pages/availability/[schedule].tsx +++ b/apps/web/pages/availability/[schedule].tsx @@ -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 ( } + heading={} subtitle={data.schedule.availability.map((availability) => ( {availabilityAsString(availability, i18n.language)} diff --git a/apps/web/pages/availability/index.tsx b/apps/web/pages/availability/index.tsx index 3653344840..8181ed674e 100644 --- a/apps/web/pages/availability/index.tsx +++ b/apps/web/pages/availability/index.tsx @@ -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"; diff --git a/apps/web/pages/availability/troubleshoot.tsx b/apps/web/pages/availability/troubleshoot.tsx index eafa6195cb..0d0de59433 100644 --- a/apps/web/pages/availability/troubleshoot.tsx +++ b/apps/web/pages/availability/troubleshoot.tsx @@ -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"; diff --git a/apps/web/pages/bookings/[status].tsx b/apps/web/pages/bookings/[status].tsx index 944fa9be14..418f1fc107 100644 --- a/apps/web/pages/bookings/[status].tsx +++ b/apps/web/pages/bookings/[status].tsx @@ -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 ( }> - +
diff --git a/apps/web/pages/event-types/[type].tsx b/apps/web/pages/event-types/[type].tsx index 353d04c1fb..b87460dddb 100644 --- a/apps/web/pages/event-types/[type].tsx +++ b/apps/web/pages/event-types/[type].tsx @@ -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) => { 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(undefined); const [selectedCustomInput, setSelectedCustomInput] = useState(undefined); @@ -315,7 +318,6 @@ const EventTypePage = (props: inferSSRProps) => { const [customInputs, setCustomInputs] = useState( eventType.customInputs.sort((a, b) => a.id - b.id) || [] ); - const [tokensList, setTokensList] = useState>([]); const defaultSeatsPro = 6; const minSeats = 2; @@ -351,7 +353,6 @@ const EventTypePage = (props: inferSSRProps) => { async function deleteEventTypeHandler(event: React.MouseEvent) { event.preventDefault(); - const payload = { id: eventType.id }; deleteMutation.mutate(payload); } @@ -437,6 +438,7 @@ const EventTypePage = (props: inferSSRProps) => { const formMethods = useForm({ defaultValues: { + title: eventType.title, locations: eventType.locations || [], recurringEvent: eventType.recurringEvent || null, schedule: eventType.schedule?.id, @@ -844,37 +846,10 @@ const EventTypePage = (props: inferSSRProps) => { setEditIcon(false)}> - {editIcon ? ( - <> -

- {formMethods.getValues("title") && formMethods.getValues("title") !== "" - ? formMethods.getValues("title") - : eventType.title} -

- - - ) : ( -
- { - setEditIcon(true); - formMethods.getValues("title") === "" && formMethods.setValue("title", eventType.title); - }} - /> -
- )} -
+ formMethods.setValue("title", value)} + /> } subtitle={eventType.description || ""}> }> @@ -1978,20 +1953,24 @@ const EventTypePage = (props: inferSSRProps) => { 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} /> - - - - {t("delete")} - - - {t("delete_event_type_description")} - - + {/* 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) && ( + + + + {t("delete")} + + + {t("delete_event_type_description")} + + + )}
@@ -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, }, }; }; diff --git a/apps/web/pages/event-types/index.tsx b/apps/web/pages/event-types/index.tsx index b9bc4dcacb..b7042c7de8 100644 --- a/apps/web/pages/event-types/index.tsx +++ b/apps/web/pages/event-types/index.tsx @@ -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 />
- - - + {/* 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) && ( + + + + )} diff --git a/apps/web/pages/getting-started.tsx b/apps/web/pages/getting-started.tsx index e8a9682347..06947fb8e9 100644 --- a/apps/web/pages/getting-started.tsx +++ b/apps/web/pages/getting-started.tsx @@ -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"; diff --git a/apps/web/pages/settings/developer.tsx b/apps/web/pages/settings/developer.tsx index b032c4be5f..2b639a4934 100644 --- a/apps/web/pages/settings/developer.tsx +++ b/apps/web/pages/settings/developer.tsx @@ -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 ( - + + {routingForms && ( + + )} ); diff --git a/apps/web/pages/settings/profile.tsx b/apps/web/pages/settings/profile.tsx index a571b0652d..995f914864 100644 --- a/apps/web/pages/settings/profile.tsx +++ b/apps/web/pages/settings/profile.tsx @@ -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; diff --git a/apps/web/pages/settings/security.tsx b/apps/web/pages/settings/security.tsx index b7493a8f4d..bb2f0eab04 100644 --- a/apps/web/pages/settings/security.tsx +++ b/apps/web/pages/settings/security.tsx @@ -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"; diff --git a/apps/web/pages/settings/teams/[id]/index.tsx b/apps/web/pages/settings/teams/[id]/index.tsx index 2f1a4d417e..2d12a1a7c7 100644 --- a/apps/web/pages/settings/teams/[id]/index.tsx +++ b/apps/web/pages/settings/teams/[id]/index.tsx @@ -4,6 +4,7 @@ import { useRouter } from "next/router"; import { useEffect, useState } from "react"; import showToast from "@calcom/lib/notification"; +import { trpc } from "@calcom/trpc/react"; import { SkeletonAvatar, SkeletonText } from "@calcom/ui"; import { Alert } from "@calcom/ui/Alert"; import { Button } from "@calcom/ui/Button"; @@ -11,10 +12,11 @@ import SAMLConfiguration from "@ee/components/saml/Configuration"; import { QueryCell } from "@lib/QueryCell"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; +import useCurrentUserId from "@lib/hooks/useCurrentUserId"; import { useLocale } from "@lib/hooks/useLocale"; -import { trpc } from "@lib/trpc"; import Shell from "@components/Shell"; +import DisableTeamImpersonation from "@components/team/DisableTeamImpersonation"; import MemberInvitationModal from "@components/team/MemberInvitationModal"; import MemberList from "@components/team/MemberList"; import TeamSettings from "@components/team/TeamSettings"; @@ -25,6 +27,7 @@ import Avatar from "@components/ui/Avatar"; export function TeamSettingsPage() { const { t } = useLocale(); const router = useRouter(); + const userId = useCurrentUserId(); const upgraded = router.query.upgraded as string; @@ -165,6 +168,7 @@ export function TeamSettingsPage() { {isAdmin && } + {userId && }
diff --git a/apps/web/pages/settings/teams/index.tsx b/apps/web/pages/settings/teams/index.tsx index 04f4e526be..e110def713 100644 --- a/apps/web/pages/settings/teams/index.tsx +++ b/apps/web/pages/settings/teams/index.tsx @@ -5,12 +5,12 @@ import { Trans } from "next-i18next"; import { useState } from "react"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; import { Alert } from "@calcom/ui/Alert"; import Button from "@calcom/ui/Button"; import EmptyScreen from "@calcom/ui/EmptyScreen"; import useMeQuery from "@lib/hooks/useMeQuery"; -import { trpc } from "@lib/trpc"; import SettingsShell from "@components/SettingsShell"; import SkeletonLoaderTeamList from "@components/team/SkeletonloaderTeamList"; diff --git a/apps/web/pages/success.tsx b/apps/web/pages/success.tsx index 79a9196968..3575e84c61 100644 --- a/apps/web/pages/success.tsx +++ b/apps/web/pages/success.tsx @@ -425,7 +425,7 @@ export default function Success(props: SuccessProps) { <>
{t("or_lowercase")}
- {t("Reschedule")} + {t("reschedule")}
)} diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx index d90d07f595..3510dbcce8 100644 --- a/apps/web/pages/team/[slug].tsx +++ b/apps/web/pages/team/[slug].tsx @@ -8,6 +8,7 @@ import React, { useEffect } from "react"; import { useIsEmbed } from "@calcom/embed-core/embed-iframe"; import { CAL_URL } from "@calcom/lib/constants"; +import { getTeamWithMembers } from "@calcom/lib/server/queries/teams"; import Button from "@calcom/ui/Button"; import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar"; @@ -15,7 +16,6 @@ import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally"; import { useLocale } from "@lib/hooks/useLocale"; import useTheme from "@lib/hooks/useTheme"; import { useToggleQuery } from "@lib/hooks/useToggleQuery"; -import { getTeamWithMembers } from "@lib/queries/teams"; import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@lib/telemetry"; import { inferSSRProps } from "@lib/types/inferSSRProps"; diff --git a/apps/web/playwright/embed-code-generator.test.ts b/apps/web/playwright/embed-code-generator.test.ts index 4950a706ed..f0582e3a1e 100644 --- a/apps/web/playwright/embed-code-generator.test.ts +++ b/apps/web/playwright/embed-code-generator.test.ts @@ -158,8 +158,10 @@ test.describe("Embed Code Generator Tests", () => { }); test.describe("Event Type Edit Page", () => { + //TODO: Instead of hardcoding, browse through actual events, as this ID might change in future + const sixtyMinProEventId = "6"; test.beforeEach(async ({ page }) => { - await page.goto("/event-types/3"); + await page.goto(`/event-types/${sixtyMinProEventId}`); }); test("open Embed Dialog for the Event Type", async ({ page }) => { @@ -167,14 +169,14 @@ test.describe("Embed Code Generator Tests", () => { await expectToBeNavigatingToEmbedTypesDialog(page, { eventTypeId, - basePage: "/event-types/3", + basePage: `/event-types/${sixtyMinProEventId}`, }); chooseEmbedType(page, "inline"); await expectToBeNavigatingToEmbedCodeAndPreviewDialog(page, { eventTypeId, - basePage: "/event-types/3", + basePage: `/event-types/${sixtyMinProEventId}`, embedType: "inline", }); @@ -186,7 +188,7 @@ test.describe("Embed Code Generator Tests", () => { await expectToContainValidPreviewIframe(page, { embedType: "inline", - calLink: "pro/30min", + calLink: "pro/60min", }); }); }); diff --git a/apps/web/playwright/event-types.test.ts b/apps/web/playwright/event-types.test.ts index bfd39ab2c1..db24426031 100644 --- a/apps/web/playwright/event-types.test.ts +++ b/apps/web/playwright/event-types.test.ts @@ -91,12 +91,14 @@ test.describe("Event Types tests", () => { }); test("can duplicate an existing event type", async ({ page }) => { - const firstTitle = await page.locator("[data-testid=event-type-title-3]").innerText(); - const firstFullSlug = await page.locator("[data-testid=event-type-slug-3]").innerText(); + // TODO: Locate the actual EventType available in list. This ID might change in future + const eventTypeId = "6"; + const firstTitle = await page.locator(`[data-testid=event-type-title-${eventTypeId}]`).innerText(); + const firstFullSlug = await page.locator(`[data-testid=event-type-slug-${eventTypeId}]`).innerText(); const firstSlug = firstFullSlug.split("/")[2]; - await page.click("[data-testid=event-type-options-3]"); - await page.click("[data-testid=event-type-duplicate-3]"); + await page.click(`[data-testid=event-type-options-${eventTypeId}]`); + await page.click(`[data-testid=event-type-duplicate-${eventTypeId}]`); const url = page.url(); const params = new URLSearchParams(url); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 990879319a..62881b9158 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -344,6 +344,7 @@ "booking_cancelled": "Booking Cancelled", "booking_rescheduled": "Booking Rescheduled", "booking_created": "Booking Created", + "form_submitted": "Form Submitted", "event_triggers": "Event Triggers", "subscriber_url": "Subscriber Url", "create_new_webhook": "Create a new webhook", @@ -377,6 +378,7 @@ "enable": "Enable", "code": "Code", "code_is_incorrect": "Code is incorrect.", + "add_time_availability": "Add new time slot", "add_an_extra_layer_of_security": "Add an extra layer of security to your account in case your password is stolen.", "2fa": "Two-Factor Authentication", "enable_2fa": "Enable two-factor authentication", @@ -836,6 +838,7 @@ "impersonate": "Impersonate", "user_impersonation_heading": "User Impersonation", "user_impersonation_description": "Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.", + "team_impersonation_description": "Allows your team admins to temporarily sign in as you.", "impersonate_user_tip": "All uses of this feature is audited.", "impersonating_user_warning": "Impersonating username \"{{user}}\".", "impersonating_stop_instructions": "<0>Click Here to stop.", @@ -952,7 +955,7 @@ "nr_event_type_one": "{{count}} Event Type", "nr_event_type_other": "{{count}} Event Types", "add_action": "Add action", - "set_whereby_link":"Set Whereby link", + "set_whereby_link": "Set Whereby link", "invalid_whereby_link": "Please enter a valid Whereby Link", "set_around_link": "Set Around.Co link", "invalid_around_link": "Please enter a valid Around Link", @@ -993,6 +996,7 @@ "open": "Open", "close": "Close", "pro_feature_teams": "This is a Pro feature. Upgrade to Pro to see your team's availability.", - "pro_feature_workflows": "This is a Pro feature. Upgrade to Pro to automate your event notifications and reminders with Workflows." - + "pro_feature_workflows": "This is a Pro feature. Upgrade to Pro to automate your event notifications and reminders with Workflows.", + "current_username": "Current Username", + "new_username": "New Username" } diff --git a/apps/web/public/static/locales/es/common.json b/apps/web/public/static/locales/es/common.json index 24a97916bb..5ff9d02b60 100644 --- a/apps/web/public/static/locales/es/common.json +++ b/apps/web/public/static/locales/es/common.json @@ -92,7 +92,7 @@ "meeting_password": "Contraseña de la reunión", "meeting_url": "URL de la reunión", "meeting_request_rejected": "Su solicitud de reunión ha sido rechazada", - "rescheduled_event_type_subject": "Reprogramado: {{eventType}} con {{name}} el {{date}}", + "rescheduled_event_type_subject": "Solicitud de reprogramación enviada: {{eventType}} con {{name}} el {{date}}", "requested_to_reschedule_subject_attendee": "Reprogramar acción requerida: reserva una nueva hora para {{eventType}} con {{name}}", "rejected_event_type_with_organizer": "Rechazado: {{eventType}} con {{organizer}} en {{date}}", "hi": "Hola", @@ -934,9 +934,9 @@ "attendee_name": "Nombre del asistente", "broken_integration": "Integración rota", "problem_adding_video_link": "Hubo un problema al añadir un enlace de vídeo", - "problem_updating_calendar": "Hubo un problema al actualizar su calendario", + "problem_updating_calendar": "Hubo un problema al actualizar tu calendario", "new_seat_subject": "Nuevo asistente {{name}} en {{eventType}} el {{date}}", "new_seat_title": "Alguien se ha añadido a un evento", "app_upgrade_description": "Para poder usar esta función, necesita actualizarse a una cuenta Pro.", - "invalid_number": "Número de teléfono inválido" + "invalid_number": "Número de teléfono no válido" } diff --git a/apps/web/public/static/locales/fr/common.json b/apps/web/public/static/locales/fr/common.json index 324d849350..6e55d6984d 100644 --- a/apps/web/public/static/locales/fr/common.json +++ b/apps/web/public/static/locales/fr/common.json @@ -375,6 +375,7 @@ "enable": "Activer", "code": "Code", "code_is_incorrect": "Code incorrect.", + "add_time_availability": "Ajouter une disponibilité horaire", "add_an_extra_layer_of_security": "Ajoutez une sécurité supplémentaire à votre compte en cas de vol de votre mot de passe.", "2fa": "Double authentification", "enable_2fa": "Activer la double authentification", diff --git a/apps/web/public/static/locales/pt/common.json b/apps/web/public/static/locales/pt/common.json index a56df548cd..afe0286943 100644 --- a/apps/web/public/static/locales/pt/common.json +++ b/apps/web/public/static/locales/pt/common.json @@ -286,6 +286,8 @@ "cannot_cancel_booking": "Não pode cancelar esta reserva", "reschedule_instead": "Em vez disso, também poderia reagendar", "event_is_in_the_past": "O evento já passou", + "cancelling_event_recurring": "O evento é uma instância de um evento recorrente.", + "cancelling_all_recurring": "Estas são todas as restantes instâncias do evento recorrente.", "error_with_status_code_occured": "Ocorreu um erro com o código de estado {{status}}", "booking_already_cancelled": "Esta reserva já foi cancelada", "go_back_home": "Voltar à Página Inicial", @@ -342,6 +344,7 @@ "booking_cancelled": "Reserva Cancelada", "booking_rescheduled": "Reserva Reagendada", "booking_created": "Reserva Criada", + "form_submitted": "Formulário enviado", "event_triggers": "Causadores de eventos", "subscriber_url": "URL do assinante", "create_new_webhook": "Criar um novo webhook", @@ -375,6 +378,7 @@ "enable": "Ativar", "code": "Código", "code_is_incorrect": "O código está incorreto.", + "add_time_availability": "Adicionar novo horário", "add_an_extra_layer_of_security": "Adicione uma camada extra de segurança à sua conta, caso a sua senha seja roubada.", "2fa": "Autenticação com dois fatores", "enable_2fa": "Ativar autenticação de dois fatores", @@ -834,6 +838,7 @@ "impersonate": "Representar", "user_impersonation_heading": "Representação de utilizador", "user_impersonation_description": "Permite à nossa equipa de suporte iniciar sessão temporariamente em seu nome para rapidamente resolver quaisquer erros que nos reporte.", + "team_impersonation_description": "Permite que os administradores da sua equipa iniciem sessão temporariamente em seu nome.", "impersonate_user_tip": "Todas as utilizações desta funcionalidade são auditadas.", "impersonating_user_warning": "Representar o nome de utilizador \"{{user}}\".", "impersonating_stop_instructions": "<0>Clique aqui para parar.", @@ -991,5 +996,7 @@ "open": "Abrir", "close": "Fechar", "pro_feature_teams": "Esta é uma funcionalidade Pro. Actualize para o Pro para ver a disponibilidade da sua equipa.", - "pro_feature_workflows": "Esta é uma funcionalidade Pro. Actualize para o Pro para automatizar as suas notificações de eventos e lembretes com os fluxos de trabalho." + "pro_feature_workflows": "Esta é uma funcionalidade Pro. Actualize para o Pro para automatizar as suas notificações de eventos e lembretes com os fluxos de trabalho.", + "current_username": "Nome de utilizador actual", + "new_username": "Novo nome de utilizador" } diff --git a/apps/web/server/lib/i18n.ts b/apps/web/server/lib/i18n.ts index 19528ab7d0..0a3f56aead 100644 --- a/apps/web/server/lib/i18n.ts +++ b/apps/web/server/lib/i18n.ts @@ -1,18 +1 @@ -import i18next from "i18next"; -import { i18n as nexti18next } from "next-i18next"; -import { serverSideTranslations } from "next-i18next/serverSideTranslations"; - -export const getTranslation = async (locale: string, ns: string) => { - const create = async () => { - const { _nextI18Next } = await serverSideTranslations(locale, [ns]); - const _i18n = i18next.createInstance(); - _i18n.init({ - lng: locale, - resources: _nextI18Next.initialI18nStore, - fallbackLng: _nextI18Next.userConfig?.i18n.defaultLocale, - }); - return _i18n; - }; - const _i18n = nexti18next != null ? nexti18next : await create(); - return _i18n.getFixedT(locale, ns); -}; +export * from "@calcom/lib/server/i18n"; diff --git a/apps/web/server/lib/ssg.ts b/apps/web/server/lib/ssg.ts index a7abd8668d..090adbc3c1 100644 --- a/apps/web/server/lib/ssg.ts +++ b/apps/web/server/lib/ssg.ts @@ -3,10 +3,9 @@ import { i18n } from "next-i18next.config"; import { serverSideTranslations } from "next-i18next/serverSideTranslations"; import superjson from "superjson"; -import prisma from "@lib/prisma"; - -import { appRouter } from "@server/routers/_app"; -import { createSSGHelpers } from "@trpc/react/ssg"; +import prisma from "@calcom/prisma"; +import { createSSGHelpers } from "@calcom/trpc/react/ssg"; +import { appRouter } from "@calcom/trpc/server/routers/_app"; /** * Initialize static site rendering tRPC helpers. diff --git a/apps/web/server/lib/ssr.ts b/apps/web/server/lib/ssr.ts index c57a0a5111..80b8108da8 100644 --- a/apps/web/server/lib/ssr.ts +++ b/apps/web/server/lib/ssr.ts @@ -1,10 +1,9 @@ import { GetServerSidePropsContext } from "next"; import superjson from "superjson"; -import { createContext } from "@server/createContext"; -import { createSSGHelpers } from "@trpc/react/ssg"; - -import { appRouter } from "../routers/_app"; +import { createSSGHelpers } from "@calcom/trpc/react/ssg"; +import { createContext } from "@calcom/trpc/server/createContext"; +import { appRouter } from "@calcom/trpc/server/routers/_app"; /** * Initialize server-side rendering tRPC helpers. diff --git a/apps/web/server/routers/viewer/slots.tsx b/apps/web/server/routers/viewer/slots.tsx deleted file mode 100644 index 58fcef5064..0000000000 --- a/apps/web/server/routers/viewer/slots.tsx +++ /dev/null @@ -1,264 +0,0 @@ -import { SchedulingType } from "@prisma/client"; -import { z } from "zod"; - -import type { CurrentSeats } from "@calcom/core/getUserAvailability"; -import { getUserAvailability } from "@calcom/core/getUserAvailability"; -import dayjs, { Dayjs } from "@calcom/dayjs"; -import logger from "@calcom/lib/logger"; -import { availabilityUserSelect } from "@calcom/prisma"; -import { TimeRange } from "@calcom/types/schedule"; - -import isOutOfBounds from "@lib/isOutOfBounds"; -import getSlots from "@lib/slots"; - -import { createRouter } from "@server/createRouter"; -import { TRPCError } from "@trpc/server"; - -const getScheduleSchema = z - .object({ - // startTime ISOString - startTime: z.string(), - // endTime ISOString - endTime: z.string(), - // Event type ID - eventTypeId: z.number().optional(), - // invitee timezone - timeZone: z.string().optional(), - // or list of users (for dynamic events) - usernameList: z.array(z.string()).optional(), - debug: z.boolean().optional(), - }) - .refine( - (data) => !!data.eventTypeId || !!data.usernameList, - "Either usernameList or eventTypeId should be filled in." - ); - -export type Slot = { - time: string; - attendees?: number; - bookingUid?: string; - users?: string[]; -}; - -const checkForAvailability = ({ - time, - busy, - eventLength, - beforeBufferTime, - currentSeats, -}: { - time: Dayjs; - busy: (TimeRange | { start: string; end: string })[]; - eventLength: number; - beforeBufferTime: number; - currentSeats?: CurrentSeats; -}) => { - if (currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString())) { - return true; - } - - const slotEndTime = time.add(eventLength, "minutes").utc(); - const slotStartTime = time.subtract(beforeBufferTime, "minutes").utc(); - - return busy.every((busyTime) => { - const startTime = dayjs.utc(busyTime.start); - const endTime = dayjs.utc(busyTime.end); - - if (endTime.isBefore(slotStartTime) || startTime.isAfter(slotEndTime)) { - return true; - } - - if (slotStartTime.isBetween(startTime, endTime, null, "[)")) { - return false; - } else if (slotEndTime.isBetween(startTime, endTime, null, "(]")) { - return false; - } - - // Check if start times are the same - if (time.utc().isBetween(startTime, endTime, null, "[)")) { - return false; - } - // Check if slot end time is between start and end time - else if (slotEndTime.isBetween(startTime, endTime)) { - return false; - } - // Check if startTime is between slot - else if (startTime.isBetween(time, slotEndTime)) { - return false; - } - - return true; - }); -}; - -export const slotsRouter = createRouter().query("getSchedule", { - input: getScheduleSchema, - async resolve({ input, ctx }) { - if (input.debug === true) { - logger.setSettings({ minLevel: "debug" }); - } - const startPrismaEventTypeGet = performance.now(); - const eventType = await ctx.prisma.eventType.findUnique({ - where: { - id: input.eventTypeId, - }, - select: { - id: true, - minimumBookingNotice: true, - length: true, - seatsPerTimeSlot: true, - timeZone: true, - slotInterval: true, - beforeEventBuffer: true, - afterEventBuffer: true, - schedulingType: true, - periodType: true, - periodStartDate: true, - periodEndDate: true, - periodCountCalendarDays: true, - periodDays: true, - schedule: { - select: { - availability: true, - timeZone: true, - }, - }, - availability: { - select: { - startTime: true, - endTime: true, - days: true, - }, - }, - users: { - select: { - username: true, - ...availabilityUserSelect, - }, - }, - }, - }); - const endPrismaEventTypeGet = performance.now(); - logger.debug(`Prisma eventType get took ${endPrismaEventTypeGet - startPrismaEventTypeGet}ms`); - if (!eventType) { - throw new TRPCError({ code: "NOT_FOUND" }); - } - - const startTime = - input.timeZone === "Etc/GMT" - ? dayjs.utc(input.startTime) - : dayjs(input.startTime).utc().tz(input.timeZone); - const endTime = - input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone); - - if (!startTime.isValid() || !endTime.isValid()) { - throw new TRPCError({ message: "Invalid time range given.", code: "BAD_REQUEST" }); - } - let currentSeats: CurrentSeats | undefined = undefined; - - const userSchedules = await Promise.all( - eventType.users.map(async (currentUser) => { - const { - busy, - workingHours, - currentSeats: _currentSeats, - } = await getUserAvailability( - { - userId: currentUser.id, - dateFrom: startTime.format(), - dateTo: endTime.format(), - eventTypeId: input.eventTypeId, - afterEventBuffer: eventType.afterEventBuffer, - }, - { user: currentUser, eventType, currentSeats } - ); - if (!currentSeats && _currentSeats) currentSeats = _currentSeats; - - return { - workingHours, - busy, - }; - }) - ); - - const workingHours = userSchedules.flatMap((s) => s.workingHours); - - const slots: Record = {}; - const availabilityCheckProps = { - eventLength: eventType.length, - beforeBufferTime: eventType.beforeEventBuffer, - currentSeats, - }; - const isWithinBounds = (_time: Parameters[0]) => - !isOutOfBounds(_time, { - periodType: eventType.periodType, - periodStartDate: eventType.periodStartDate, - periodEndDate: eventType.periodEndDate, - periodCountCalendarDays: eventType.periodCountCalendarDays, - periodDays: eventType.periodDays, - }); - - let time = startTime; - let getSlotsTime = 0; - let checkForAvailabilityTime = 0; - let getSlotsCount = 0; - let checkForAvailabilityCount = 0; - do { - const startGetSlots = performance.now(); - // get slots retrieves the available times for a given day - const times = getSlots({ - inviteeDate: time, - eventLength: eventType.length, - workingHours, - minimumBookingNotice: eventType.minimumBookingNotice, - frequency: eventType.slotInterval || eventType.length, - }); - const endGetSlots = performance.now(); - getSlotsTime += endGetSlots - startGetSlots; - getSlotsCount++; - // if ROUND_ROBIN - slots stay available on some() - if normal / COLLECTIVE - slots only stay available on every() - const filterStrategy = - !eventType.schedulingType || eventType.schedulingType === SchedulingType.COLLECTIVE - ? ("every" as const) - : ("some" as const); - - const filteredTimes = times.filter(isWithinBounds).filter((time) => - userSchedules[filterStrategy]((schedule) => { - const startCheckForAvailability = performance.now(); - const result = checkForAvailability({ time, ...schedule, ...availabilityCheckProps }); - const endCheckForAvailability = performance.now(); - checkForAvailabilityCount++; - checkForAvailabilityTime += endCheckForAvailability - startCheckForAvailability; - return result; - }) - ); - - slots[time.format("YYYY-MM-DD")] = filteredTimes.map((time) => ({ - time: time.toISOString(), - users: eventType.users.map((user) => user.username || ""), - // Conditionally add the attendees and booking id to slots object if there is already a booking during that time - ...(currentSeats?.some((booking) => booking.startTime.toISOString() === time.toISOString()) && { - attendees: - currentSeats[ - currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) - ]._count.attendees, - bookingUid: - currentSeats[ - currentSeats.findIndex((booking) => booking.startTime.toISOString() === time.toISOString()) - ].uid, - }), - })); - time = time.add(1, "day"); - } while (time.isBefore(endTime)); - - logger.debug(`getSlots took ${getSlotsTime}ms and executed ${getSlotsCount} times`); - - logger.debug( - `checkForAvailability took ${checkForAvailabilityTime}ms and executed ${checkForAvailabilityCount} times` - ); - - return { - slots, - }; - }, -}); diff --git a/apps/web/test/.env.test.example b/apps/web/test/.env.test.example new file mode 100644 index 0000000000..ee9ed1cf0e --- /dev/null +++ b/apps/web/test/.env.test.example @@ -0,0 +1,3 @@ +DATABASE_URL="postgresql://prisma:prisma@localhost:5433/tests" +NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000" +INTEGRATION_TEST_MODE=true \ No newline at end of file diff --git a/apps/web/test/README.md b/apps/web/test/README.md new file mode 100644 index 0000000000..18cf96aa3b --- /dev/null +++ b/apps/web/test/README.md @@ -0,0 +1,12 @@ +# Unit and Integration Tests + +Make sure you have copied .env.test.example to .env.test + +You can run all jest tests as + +`yarn test` + +You can run tests matching specific description by following command +`yarn test -t getSchedule` + +Tip: Use `--watchAll` flag to run tests on every change \ No newline at end of file diff --git a/apps/web/test/docker-compose.yml b/apps/web/test/docker-compose.yml new file mode 100644 index 0000000000..769adab097 --- /dev/null +++ b/apps/web/test/docker-compose.yml @@ -0,0 +1,15 @@ +# Set the version of docker compose to use +version: '3.9' + +# The containers that compose the project +services: + db: + image: postgres:13 + restart: always + container_name: integration-tests-prisma + ports: + - '5433:5432' + environment: + POSTGRES_USER: prisma + POSTGRES_PASSWORD: prisma + POSTGRES_DB: tests \ No newline at end of file diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts new file mode 100644 index 0000000000..15b92e7007 --- /dev/null +++ b/apps/web/test/lib/getSchedule.test.ts @@ -0,0 +1,578 @@ +import { Prisma } from "@prisma/client"; +import nock from "nock"; +import { v4 as uuidv4 } from "uuid"; + +import logger from "@calcom/lib/logger"; +import prisma from "@calcom/prisma"; +import { BookingStatus, PeriodType } from "@calcom/prisma/client"; +import { getSchedule } from "@calcom/trpc/server/routers/viewer/slots"; + +declare global { + // eslint-disable-next-line @typescript-eslint/no-namespace + namespace jest { + interface Matchers { + toHaveTimeSlots(expectedSlots: string[], date: { dateString: string }): R; + } + } +} + +expect.extend({ + toHaveTimeSlots(schedule, expectedSlots: string[], { dateString }: { dateString: string }) { + expect(schedule.slots[`${dateString}`]).toBeDefined(); + expect(schedule.slots[`${dateString}`].map((slot: { time: string }) => slot.time)).toEqual( + expectedSlots.map((slotTime) => `${dateString}T${slotTime}`) + ); + return { + pass: true, + message: () => "has correct timeslots ", + }; + }, +}); + +/** + * This fn indents to dynamically compute day, month, year for the purpose of testing. + * We are not using DayJS because that's actually being tested by this code. + */ +const getDate = (param: { dateIncrement?: number; monthIncrement?: number; yearIncrement?: number } = {}) => { + let { dateIncrement, monthIncrement, yearIncrement } = param; + dateIncrement = dateIncrement || 0; + monthIncrement = monthIncrement || 0; + yearIncrement = yearIncrement || 0; + const year = new Date().getFullYear() + yearIncrement; + // Make it start with 1 to match with DayJS requiremet + let _month = new Date().getMonth() + monthIncrement + 1; + if (_month === 13) { + _month = 1; + } + const month = _month < 10 ? "0" + _month : _month; + + let _date = new Date().getDate() + dateIncrement; + + // If last day of the month(As _month is plus 1 already it is going to be the 0th day of next month which is the last day of current month) + if (_date === new Date(year, _month, 0).getDate()) { + _date = 1; + } + + const date = _date < 10 ? "0" + _date : _date; + // console.log("Date, month, year:", date, month, year); + return { + date, + month, + year, + dateString: `${year}-${month}-${date}`, + }; +}; + +const ctx = { + prisma, +}; + +type App = { + slug: string; + dirName: string; +}; +type User = { + credentials?: Credential[]; + selectedCalendars?: SelectedCalendar[]; +}; + +type Credential = { key: any; type: string }; +type SelectedCalendar = { + integration: string; + externalId: string; +}; + +type EventType = { + id?: number; + title?: string; + length: number; + periodType: PeriodType; + slotInterval: number; + minimumBookingNotice: number; + seatsPerTimeSlot?: number | null; +}; + +type Booking = { + userId: number; + eventTypeId: number; + startTime: string; + endTime: string; + title?: string; + status: BookingStatus; +}; + +function getGoogleCalendarCredential() { + return { + type: "google_calendar", + key: { + scope: + "https://www.googleapis.com/auth/calendar.events https://www.googleapis.com/auth/calendar.readonly", + token_type: "Bearer", + expiry_date: 1656999025367, + access_token: "ACCESS_TOKEN", + refresh_token: "REFRESH_TOKEN", + }, + }; +} + +async function addEventTypeToDB(data: { + eventType: EventType; + selectedCalendars?: SelectedCalendar[]; + credentials?: Credential[]; + users?: User[]; + usersConnectedToTheEvent?: { id: number }[]; + numUsers?: number; +}) { + data.selectedCalendars = data.selectedCalendars || []; + data.credentials = data.credentials || []; + const userCreate = { + id: 100, + username: "hariom", + email: "hariombalhara@gmail.com", + schedules: { + create: { + name: "Schedule1", + availability: { + create: { + userId: null, + eventTypeId: null, + days: [0, 1, 2, 3, 4, 5, 6], + startTime: "1970-01-01T09:30:00.000Z", + endTime: "1970-01-01T18:00:00.000Z", + date: null, + }, + }, + timeZone: "Asia/Kolkata", + }, + }, + }; + const usersCreate: typeof userCreate[] = []; + + if (!data.users && !data.numUsers && !data.usersConnectedToTheEvent) { + throw new Error("Either users, numUsers or usersConnectedToTheEvent must be provided"); + } + if (!data.users && data.numUsers) { + data.users = []; + for (let i = 0; i < data.numUsers; i++) { + data.users.push({ + credentials: undefined, + selectedCalendars: undefined, + }); + } + } + + if (data.users?.length) { + data.users.forEach((user, index) => { + const newUserCreate = { + ...userCreate, + ...user, + credentials: { create: user.credentials }, + selectedCalendars: { create: user.selectedCalendars }, + }; + newUserCreate.id = index + 1; + newUserCreate.username = `IntegrationTestUser${newUserCreate.id}`; + newUserCreate.email = `IntegrationTestUser${newUserCreate.id}@example.com`; + usersCreate.push(newUserCreate); + }); + } else { + usersCreate.push({ ...userCreate }); + } + + const prismaData: Prisma.EventTypeCreateArgs["data"] = { + title: "Test EventType Title", + slug: "testslug", + timeZone: null, + beforeEventBuffer: 0, + afterEventBuffer: 0, + schedulingType: null, + periodStartDate: "2022-01-21T09:03:48.000Z", + periodEndDate: "2022-01-21T09:03:48.000Z", + periodCountCalendarDays: false, + periodDays: 30, + users: { + create: usersCreate, + connect: data.usersConnectedToTheEvent, + }, + ...data.eventType, + }; + logger.silly("TestData: Creating EventType", prismaData); + + return await prisma.eventType.create({ + data: prismaData, + select: { + id: true, + users: true, + }, + }); +} + +async function addBookingToDB(data: { booking: Booking }) { + const prismaData = { + uid: uuidv4(), + title: "Test Booking Title", + ...data.booking, + }; + logger.silly("TestData: Creating Booking", prismaData); + + return await prisma.booking.create({ + data: prismaData, + }); +} + +async function createBookingScenario(data: { + booking?: Omit; + users?: User[]; + numUsers?: number; + credentials?: Credential[]; + apps?: App[]; + selectedCalendars?: SelectedCalendar[]; + eventType: EventType; + /** + * User must already be existing + * */ + usersConnectedToTheEvent?: { id: number }[]; +}) { + // if (!data.eventType.userId) { + // data.eventType.userId = + // (data.users ? data.users[0]?.id : null) || data.usersConnect ? data.usersConnect[0]?.id : null; + // } + const eventType = await addEventTypeToDB(data); + if (data.apps) { + await prisma.app.createMany({ + data: data.apps, + }); + } + if (data.booking) { + // TODO: What about if there are multiple users of the eventType? + const userId = eventType.users[0].id; + const eventTypeId = eventType.id; + + await addBookingToDB({ ...data, booking: { ...data.booking, userId, eventTypeId } }); + } + return { + eventType, + }; +} + +const cleanup = async () => { + await prisma.eventType.deleteMany(); + await prisma.user.deleteMany(); + await prisma.schedule.deleteMany(); + await prisma.selectedCalendar.deleteMany(); + await prisma.credential.deleteMany(); + await prisma.booking.deleteMany(); + await prisma.app.deleteMany(); +}; + +beforeEach(async () => { + await cleanup(); +}); + +afterEach(async () => { + await cleanup(); +}); + +describe("getSchedule", () => { + describe("User Event", () => { + test("correctly identifies unavailable slots from Cal Bookings", async () => { + // const { dateString: todayDateString } = getDate(); + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); + const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); + + // An event with one accepted booking + const { eventType } = await createBookingScenario({ + eventType: { + minimumBookingNotice: 1440, + length: 30, + slotInterval: 45, + periodType: "UNLIMITED" as PeriodType, + }, + numUsers: 1, + booking: { + status: "ACCEPTED", + startTime: `${plus3DateString}T04:00:00.000Z`, + endTime: `${plus3DateString}T04:15:00.000Z`, + }, + }); + + // const scheduleLyingWithinMinBookingNotice = await getSchedule( + // { + // eventTypeId: eventType.id, + // startTime: `${todayDateString}T18:30:00.000Z`, + // endTime: `${plus1DateString}T18:29:59.999Z`, + // timeZone: "Asia/Kolkata", + // }, + // ctx + // ); + + // expect(scheduleLyingWithinMinBookingNotice).toHaveTimeSlots([], { + // dateString: plus1DateString, + // }); + + const scheduleOnCompletelyFreeDay = await getSchedule( + { + eventTypeId: eventType.id, + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: "Asia/Kolkata", + }, + ctx + ); + + expect(scheduleOnCompletelyFreeDay).toHaveTimeSlots( + [ + "04:00:00.000Z", + "04:45:00.000Z", + "05:30:00.000Z", + "06:15:00.000Z", + "07:00:00.000Z", + "07:45:00.000Z", + "08:30:00.000Z", + "09:15:00.000Z", + "10:00:00.000Z", + "10:45:00.000Z", + "11:30:00.000Z", + ], + { + dateString: plus2DateString, + } + ); + + const scheduleForDayWithOneBooking = await getSchedule( + { + eventTypeId: eventType.id, + startTime: `${plus2DateString}T18:30:00.000Z`, + endTime: `${plus3DateString}T18:29:59.999Z`, + timeZone: "Asia/Kolkata", // GMT+5:30 + }, + ctx + ); + expect(scheduleForDayWithOneBooking).toHaveTimeSlots( + [ + "04:45:00.000Z", + "05:30:00.000Z", + "06:15:00.000Z", + "07:00:00.000Z", + "07:45:00.000Z", + "08:30:00.000Z", + "09:15:00.000Z", + "10:00:00.000Z", + "10:45:00.000Z", + "11:30:00.000Z", + ], + { + dateString: plus3DateString, + } + ); + }); + + test("correctly identifies unavailable slots from calendar", async () => { + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); + + // An event with one accepted booking + const { eventType } = await createBookingScenario({ + eventType: { + minimumBookingNotice: 1440, + length: 30, + slotInterval: 45, + periodType: "UNLIMITED" as PeriodType, + seatsPerTimeSlot: null, + }, + users: [ + { + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [ + { + integration: "google_calendar", + externalId: "john@example.com", + }, + ], + }, + ], + apps: [ + { + slug: "google-calendar", + dirName: "whatever", + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + //@ts-ignore + keys: { + expiry_date: Infinity, + client_id: "client_id", + client_secret: "client_secret", + redirect_uris: ["http://localhost:3000/auth/callback"], + }, + }, + ], + }); + + nock("https://oauth2.googleapis.com").post("/token").reply(200, { + access_token: "access_token", + expiry_date: Infinity, + }); + + // Google Calendar with 11th July having many events + nock("https://www.googleapis.com") + .post("/calendar/v3/freeBusy") + .reply(200, { + calendars: [ + { + busy: [ + { + start: `${plus2DateString}T04:30:00.000Z`, + end: `${plus2DateString}T23:00:00.000Z`, + }, + ], + }, + ], + }); + + const scheduleForDayWithAGoogleCalendarBooking = await getSchedule( + { + eventTypeId: eventType.id, + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: "Asia/Kolkata", + }, + ctx + ); + + // As per Google Calendar Availability, only 4PM GMT slot would be available + expect(scheduleForDayWithAGoogleCalendarBooking).toHaveTimeSlots([`04:00:00.000Z`], { + dateString: plus2DateString, + }); + }); + }); + + describe("Team Event", () => { + test("correctly identifies unavailable slots from calendar", async () => { + const { dateString: todayDateString } = getDate(); + + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); + + // An event having two users with one accepted booking + const { eventType: teamEventType } = await createBookingScenario({ + eventType: { + id: 1, + minimumBookingNotice: 0, + length: 30, + slotInterval: 45, + periodType: "UNLIMITED" as PeriodType, + seatsPerTimeSlot: null, + }, + numUsers: 2, + booking: { + status: "ACCEPTED", + startTime: `${plus2DateString}T04:00:00.000Z`, + endTime: `${plus2DateString}T04:15:00.000Z`, + }, + }); + + const scheduleForTeamEventOnADayWithNoBooking = await getSchedule( + { + eventTypeId: 1, + startTime: `${todayDateString}T18:30:00.000Z`, + endTime: `${plus1DateString}T18:29:59.999Z`, + timeZone: "Asia/Kolkata", + }, + ctx + ); + + expect(scheduleForTeamEventOnADayWithNoBooking).toHaveTimeSlots( + [ + `04:00:00.000Z`, + `04:45:00.000Z`, + `05:30:00.000Z`, + `06:15:00.000Z`, + `07:00:00.000Z`, + `07:45:00.000Z`, + `08:30:00.000Z`, + `09:15:00.000Z`, + `10:00:00.000Z`, + `10:45:00.000Z`, + `11:30:00.000Z`, + ], + { + dateString: plus1DateString, + } + ); + + const scheduleForTeamEventOnADayWithOneBooking = await getSchedule( + { + eventTypeId: 1, + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: "Asia/Kolkata", + }, + ctx + ); + + expect(scheduleForTeamEventOnADayWithOneBooking).toHaveTimeSlots( + [ + `04:45:00.000Z`, + `05:30:00.000Z`, + `06:15:00.000Z`, + `07:00:00.000Z`, + `07:45:00.000Z`, + `08:30:00.000Z`, + `09:15:00.000Z`, + `10:00:00.000Z`, + `10:45:00.000Z`, + `11:30:00.000Z`, + ], + { dateString: plus2DateString } + ); + + // An event with user 2 of team event + await createBookingScenario({ + eventType: { + id: 2, + minimumBookingNotice: 0, + length: 30, + slotInterval: 45, + periodType: "UNLIMITED" as PeriodType, + seatsPerTimeSlot: null, + }, + usersConnectedToTheEvent: [ + { + id: teamEventType.users[1].id, + }, + ], + booking: { + status: "ACCEPTED", + startTime: `${plus2DateString}T05:30:00.000Z`, + endTime: `${plus2DateString}T05:45:00.000Z`, + }, + }); + + const scheduleOfTeamEventHavingAUserWithBlockedTimeInAnotherEvent = await getSchedule( + { + eventTypeId: 1, + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: "Asia/Kolkata", + }, + ctx + ); + + // A user with blocked time in another event, doesn't impact Team Event availability + expect(scheduleOfTeamEventHavingAUserWithBlockedTimeInAnotherEvent).toHaveTimeSlots( + [ + `04:45:00.000Z`, + `05:30:00.000Z`, + `06:15:00.000Z`, + `07:00:00.000Z`, + `07:45:00.000Z`, + `08:30:00.000Z`, + `09:15:00.000Z`, + `10:00:00.000Z`, + `10:45:00.000Z`, + `11:30:00.000Z`, + ], + { dateString: plus2DateString } + ); + }); + }); +}); diff --git a/apps/web/test/lib/slots.test.ts b/apps/web/test/lib/slots.test.ts index 38285b7ba9..c08324953f 100644 --- a/apps/web/test/lib/slots.test.ts +++ b/apps/web/test/lib/slots.test.ts @@ -2,10 +2,10 @@ import { expect, it } from "@jest/globals"; import MockDate from "mockdate"; import dayjs from "@calcom/dayjs"; +import getSlots from "@calcom/lib/slots"; import { MINUTES_DAY_END, MINUTES_DAY_START } from "@lib/availability"; import { getFilteredTimes } from "@lib/hooks/useSlots"; -import getSlots from "@lib/slots"; MockDate.set("2021-06-20T11:59:59Z"); diff --git a/apps/website b/apps/website index 7c307afc65..e567969feb 160000 --- a/apps/website +++ b/apps/website @@ -1 +1 @@ -Subproject commit 7c307afc6554882ac7851a8dcbf2676026a4f932 +Subproject commit e567969feb55ebcedb3061fc156a2e9bc7d4fd03 diff --git a/package.json b/package.json index 19619a5882..32b83c33c3 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,7 @@ ], "scripts": { "build": "turbo run build --scope=\"@calcom/web\" --include-dependencies", - "clean": "turbo run clean && rm -rf node_modules", + "clean": "find . -name node_modules -o -name .next -o -name .turbo -o -name dist -type d -prune | xargs rm -rf", "db-deploy": "turbo run db-deploy", "db-seed": "turbo run db-seed", "db-studio": "yarn workspace @calcom/prisma db-studio", @@ -42,7 +42,7 @@ "test-playwright": "yarn playwright test --config=tests/config/playwright.config.ts", "embed-tests-quick": "turbo run embed-tests-quick", "embed-tests": "turbo run embed-tests", - "test-e2e": "turbo run test-e2e --scope=\"@calcom/web\" --concurrency=1", + "test-e2e": "turbo run test --scope=\"@calcom/web\" && yarn turbo run test-e2e --scope=\"@calcom/web\" --concurrency=1", "type-check": "turbo run type-check", "app-store": "yarn workspace @calcom/app-store-cli cli", "app-store:build": "yarn workspace @calcom/app-store-cli build", diff --git a/packages/app-store/components.tsx b/packages/app-store/components.tsx index f9b7099a5e..18556f9af9 100644 --- a/packages/app-store/components.tsx +++ b/packages/app-store/components.tsx @@ -4,10 +4,9 @@ import { useState, useEffect, useRef } from "react"; import { WEBAPP_URL } from "@calcom/lib/constants"; import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType"; import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; import type { App } from "@calcom/types/App"; -import { trpc } from "@lib/trpc"; - import { UpgradeToProDialog } from "@components/UpgradeToProDialog"; import { InstallAppButtonMap } from "./apps.browser.generated"; diff --git a/packages/app-store/ee/routing_forms/api/responses/[formId].ts b/packages/app-store/ee/routing_forms/api/responses/[formId].ts index 9abd79c39b..af73e86941 100644 --- a/packages/app-store/ee/routing_forms/api/responses/[formId].ts +++ b/packages/app-store/ee/routing_forms/api/responses/[formId].ts @@ -38,7 +38,6 @@ async function* getResponses(formId: string) { } else { serializedValue = escapeCsvText(value); } - csvLineColumns.push(`"Field Label :=> Value"`); csvLineColumns.push(`"${label} :=> ${serializedValue}"`); } csv.push(csvLineColumns.join(",")); @@ -48,6 +47,7 @@ async function* getResponses(formId: string) { } return ""; } + export default async function handler(req: NextApiRequest, res: NextApiResponse) { const { args } = req.query; if (!args) { diff --git a/packages/app-store/ee/routing_forms/components/RoutingNavBar.tsx b/packages/app-store/ee/routing_forms/components/RoutingNavBar.tsx index 0cd701cc83..7eb792ecd4 100644 --- a/packages/app-store/ee/routing_forms/components/RoutingNavBar.tsx +++ b/packages/app-store/ee/routing_forms/components/RoutingNavBar.tsx @@ -11,7 +11,7 @@ export default function RoutingNavBar({ }) { const tabs = [ { - name: "Fields", + name: "Form", href: `${appUrl}/form-edit/${form?.id}`, }, { diff --git a/packages/app-store/ee/routing_forms/components/SideBar.tsx b/packages/app-store/ee/routing_forms/components/SideBar.tsx index c5520157c1..f0a7f0cd4c 100644 --- a/packages/app-store/ee/routing_forms/components/SideBar.tsx +++ b/packages/app-store/ee/routing_forms/components/SideBar.tsx @@ -4,9 +4,9 @@ import { useRouter } from "next/router"; import { CAL_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 { Switch } from "@calcom/ui"; import { DialogTrigger, Dialog } from "@calcom/ui/Dialog"; -import { trpc } from "@calcom/web/lib/trpc"; import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent"; diff --git a/packages/app-store/ee/routing_forms/pages/form-edit/[...appPages].tsx b/packages/app-store/ee/routing_forms/pages/form-edit/[...appPages].tsx index 5f4e1ac110..623682b95e 100644 --- a/packages/app-store/ee/routing_forms/pages/form-edit/[...appPages].tsx +++ b/packages/app-store/ee/routing_forms/pages/form-edit/[...appPages].tsx @@ -1,19 +1,20 @@ -import { TrashIcon, PlusIcon, ArrowUpIcon, CollectionIcon, ArrowDownIcon } from "@heroicons/react/solid"; +import { ArrowDownIcon, ArrowUpIcon, CollectionIcon, PlusIcon, TrashIcon } from "@heroicons/react/solid"; import { useRouter } from "next/router"; -import { useForm, UseFormReturn, useFieldArray, Controller } from "react-hook-form"; +import { useEffect, useState } from "react"; +import { Controller, useFieldArray, useForm, UseFormReturn } from "react-hook-form"; import { v4 as uuidv4 } from "uuid"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import showToast from "@calcom/lib/notification"; +import { trpc } from "@calcom/trpc/react"; import { AppGetServerSidePropsContext, AppPrisma, AppUser } from "@calcom/types/AppGetServerSideProps"; -import { Button, Select, BooleanToggleGroup, EmptyScreen } from "@calcom/ui"; +import { BooleanToggleGroup, Button, EmptyScreen, Select } from "@calcom/ui"; import { Form, TextArea } from "@calcom/ui/form/fields"; -import { trpc } from "@calcom/web/lib/trpc"; import { inferSSRProps } from "@lib/types/inferSSRProps"; -import PencilEdit from "@components/PencilEdit"; +import EditableHeading from "@components/ui/EditableHeading"; import RoutingShell from "../../components/RoutingShell"; import SideBar from "../../components/SideBar"; @@ -72,6 +73,22 @@ function Field({ fn: () => void; }; }) { + const [identifier, _setIdentifier] = useState(hookForm.getValues(`${hookFieldNamespace}.identifier`)); + + const setUserChangedIdentifier = (val: string) => { + _setIdentifier(val); + // Also, update the form identifier so tha it can be persisted + hookForm.setValue(`${hookFieldNamespace}.identifier`, val); + }; + + const label = hookForm.watch(`${hookFieldNamespace}.label`); + + useEffect(() => { + if (!hookForm.getValues(`${hookFieldNamespace}.identifier`)) { + _setIdentifier(label); + } + }, [label, hookFieldNamespace, hookForm]); + return (
+
+
+ +
+
+ setUserChangedIdentifier(e.target.value)} + className="block w-full rounded-sm border-gray-300 text-sm" + /> +
+