Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c0e2b5129 | ||
|
|
cf32287109 | ||
|
|
2ef8e230c5 | ||
|
|
546d0d50c4 | ||
|
|
33e8198779 | ||
|
|
e7dd0ff3e9 | ||
|
|
1b2707d4f0 | ||
|
|
00071bcdf0 | ||
|
|
fc5cc5b8bc | ||
|
|
1e79a55825 | ||
|
|
4645bbcb75 | ||
|
|
95b903110f | ||
|
|
2917455324 | ||
|
|
d71765848c | ||
|
|
cf7b7d7d3d | ||
|
|
96aaa36751 | ||
|
|
52e74245a4 | ||
|
|
cc03b132e6 |
No files matched your search
@@ -8,6 +8,8 @@ on:
|
|||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
env:
|
||||||
|
NODE_OPTIONS: "--max-old-space-size=8192"
|
||||||
jobs:
|
jobs:
|
||||||
check-types:
|
check-types:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -20,8 +22,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Use Node 16.x
|
- name: Use Node 16.x
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
|
|
||||||
with:
|
with:
|
||||||
node-version: 16.x
|
node-version: 16.x
|
||||||
cache: "yarn"
|
cache: "yarn"
|
||||||
|
|
||||||
|
- name: Show info
|
||||||
|
run: node -e "console.log(require('v8').getHeapStatistics())"
|
||||||
|
|
||||||
- run: yarn --frozen-lockfile
|
- run: yarn --frozen-lockfile
|
||||||
- run: yarn type-check
|
- run: yarn type-check
|
||||||
+1
-1
Submodule apps/api updated: fcb9ca6ace...a36b6fa923.
@@ -6,7 +6,7 @@ title: Vercel
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
You need a PostgresDB database hosted somewhere. [Heroku](https://www.heroku.com) and [Supabase](https://supabase.com/) offer great free options.
|
You need a PostgresDB database hosted somewhere. [Supabase](https://supabase.com/) offers a great free option.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
|
|||||||
@@ -34,11 +34,11 @@ import useTheme from "@calcom/lib/hooks/useTheme";
|
|||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { Button } from "@calcom/ui/Button";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||||
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
|
||||||
import { EmailInput, Form } from "@calcom/ui/form/fields";
|
import { EmailInput, Form } from "@calcom/ui/form/fields";
|
||||||
|
import { Button } from "@calcom/ui/v2";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
@@ -199,16 +199,12 @@ const BookingPage = ({
|
|||||||
|
|
||||||
// There should only exists one default userData variable for primaryAttendee.
|
// There should only exists one default userData variable for primaryAttendee.
|
||||||
const defaultUserValues = {
|
const defaultUserValues = {
|
||||||
email: booking?.attendees[0].email
|
email: rescheduleUid
|
||||||
? booking.attendees[0].email
|
? booking?.attendees[0].email
|
||||||
: router.query.email
|
: router.query.email
|
||||||
? (router.query.email as string)
|
? (router.query.email as string)
|
||||||
: "",
|
: "",
|
||||||
name: booking?.attendees[0].name
|
name: rescheduleUid ? booking?.attendees[0].name : router.query.name ? (router.query.name as string) : "",
|
||||||
? booking.attendees[0].name
|
|
||||||
: router.query.name
|
|
||||||
? (router.query.name as string)
|
|
||||||
: "",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultValues = () => {
|
const defaultValues = () => {
|
||||||
@@ -595,9 +591,7 @@ const BookingPage = ({
|
|||||||
required
|
required
|
||||||
className={classNames(
|
className={classNames(
|
||||||
inputClassName,
|
inputClassName,
|
||||||
bookingForm.formState.errors.email
|
bookingForm.formState.errors.email && "!focus:ring-red-700 !border-red-700"
|
||||||
? "border-red-700 focus:ring-red-700"
|
|
||||||
: " border-gray-300 dark:border-gray-900"
|
|
||||||
)}
|
)}
|
||||||
placeholder="you@example.com"
|
placeholder="you@example.com"
|
||||||
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
|
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
|
||||||
@@ -756,7 +750,9 @@ const BookingPage = ({
|
|||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<ReactMultiEmail
|
<ReactMultiEmail
|
||||||
className="relative"
|
className="relative"
|
||||||
placeholder="guest@example.com"
|
placeholder={
|
||||||
|
<span className="dark:text-darkgray-600">guest@example.com</span>
|
||||||
|
}
|
||||||
emails={value}
|
emails={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
getLabel={(
|
getLabel={(
|
||||||
@@ -854,7 +850,7 @@ const BookingPage = ({
|
|||||||
type="button"
|
type="button"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
onClick={() => setGuestToggle(!guestToggle)}
|
onClick={() => setGuestToggle(!guestToggle)}
|
||||||
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mr-auto rounded-md">
|
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast dark:hover:border-darkmodebrandcontrast mr-auto dark:border-transparent">
|
||||||
<Icon.FiUserPlus className="mr-2" />
|
<Icon.FiUserPlus className="mr-2" />
|
||||||
{t("additional_guests")}
|
{t("additional_guests")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -864,12 +860,12 @@ const BookingPage = ({
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => router.back()}
|
onClick={() => router.back()}
|
||||||
// We override this for this component only for now - as we don't support darkmode everywhere in the app
|
// We override this for this component only for now - as we don't support darkmode everywhere in the app
|
||||||
className="dark:border-none">
|
className="dark:hover:bg-darkgray-200 dark:border-none dark:text-white">
|
||||||
{t("cancel")}
|
{t("cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast rounded-md"
|
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast dark:hover:border-darkmodebrandcontrast mr-auto dark:border-transparent"
|
||||||
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
data-testid={rescheduleUid ? "confirm-reschedule-button" : "confirm-book-button"}
|
||||||
loading={mutation.isLoading || recurringMutation.isLoading}>
|
loading={mutation.isLoading || recurringMutation.isLoading}>
|
||||||
{rescheduleUid ? t("reschedule") : t("confirm")}
|
{rescheduleUid ? t("reschedule") : t("confirm")}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { useRef, useState } from "react";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
|
||||||
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
import { Alert } from "@calcom/ui/v2/core/Alert";
|
||||||
|
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/v2/core/Dialog";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -28,15 +28,14 @@ export default function TeamCreate(props: Props) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const createTeam = (e: React.FormEvent<HTMLFormElement>) => {
|
const createTeam = () => {
|
||||||
e.preventDefault();
|
|
||||||
createTeamMutation.mutate({ name: nameRef?.current?.value });
|
createTeamMutation.mutate({ name: nameRef?.current?.value });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Dialog open={props.isOpen} onOpenChange={props.onClose}>
|
<Dialog open={props.isOpen} onOpenChange={props.onClose}>
|
||||||
<DialogContent>
|
<DialogContent type="creation" actionText={t("create_new_team")} actionOnClick={createTeam}>
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<Icon.FiUsers className="text-brandcontrast h-6 w-6" />
|
<Icon.FiUsers className="text-brandcontrast h-6 w-6" />
|
||||||
@@ -50,7 +49,7 @@ export default function TeamCreate(props: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<form onSubmit={createTeam}>
|
<form>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
|
||||||
{t("name")}
|
{t("name")}
|
||||||
@@ -66,18 +65,6 @@ export default function TeamCreate(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||||
<DialogFooter>
|
|
||||||
<Button type="button" color="secondary" onClick={props.onClose}>
|
|
||||||
{t("cancel")}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
color="primary"
|
|
||||||
className="ltr:ml-2 rtl:mr-2"
|
|
||||||
data-testid="create-new-team-button">
|
|
||||||
{t("create_team")}
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
</form>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import TeamListItem from "./TeamListItem";
|
import TeamListItem from "./TeamListItem";
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ import Link from "next/link";
|
|||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||||
import Button from "@calcom/ui/Button";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
|
||||||
|
import { Dialog, DialogTrigger } from "@calcom/ui/v2/core/Dialog";
|
||||||
import Dropdown, {
|
import Dropdown, {
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuSeparator,
|
DropdownMenuSeparator,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@calcom/ui/Dropdown";
|
} from "@calcom/ui/v2/core/Dropdown";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Tooltip } from "@calcom/ui/v2/core/Tooltip";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import Avatar from "@components/ui/Avatar";
|
import Avatar from "@components/ui/Avatar";
|
||||||
|
|
||||||
@@ -84,7 +84,7 @@ export default function TeamListItem(props: Props) {
|
|||||||
!isInvitee && "group hover:bg-neutral-50"
|
!isInvitee && "group hover:bg-neutral-50"
|
||||||
)}>
|
)}>
|
||||||
{!isInvitee ? (
|
{!isInvitee ? (
|
||||||
<Link href={"/settings/teams/" + team.id}>
|
<Link href={"/settings/teams/" + team.id + "/profile"}>
|
||||||
<a className="flex-grow cursor-pointer truncate text-sm" title={`${team.name}`}>
|
<a className="flex-grow cursor-pointer truncate text-sm" title={`${team.name}`}>
|
||||||
{teamInfo}
|
{teamInfo}
|
||||||
</a>
|
</a>
|
||||||
@@ -112,7 +112,6 @@ export default function TeamListItem(props: Props) {
|
|||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
size="sm"
|
|
||||||
className="w-full rounded-none font-medium"
|
className="w-full rounded-none font-medium"
|
||||||
StartIcon={Icon.FiCheck}
|
StartIcon={Icon.FiCheck}
|
||||||
onClick={acceptInvite}>
|
onClick={acceptInvite}>
|
||||||
@@ -121,8 +120,7 @@ export default function TeamListItem(props: Props) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Button
|
<Button
|
||||||
color="warn"
|
color="destructive"
|
||||||
size="sm"
|
|
||||||
className="w-full rounded-none font-medium"
|
className="w-full rounded-none font-medium"
|
||||||
StartIcon={Icon.FiX}
|
StartIcon={Icon.FiX}
|
||||||
onClick={declineInvite}>
|
onClick={declineInvite}>
|
||||||
@@ -158,11 +156,10 @@ export default function TeamListItem(props: Props) {
|
|||||||
<DropdownMenuContent hidden={hideDropdown}>
|
<DropdownMenuContent hidden={hideDropdown}>
|
||||||
{isAdmin && (
|
{isAdmin && (
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<Link href={"/settings/teams/" + team.id}>
|
<Link href={"/settings/teams/" + team.id + "/profile"}>
|
||||||
<a>
|
<a>
|
||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
size="sm"
|
|
||||||
className="w-full rounded-none font-medium"
|
className="w-full rounded-none font-medium"
|
||||||
StartIcon={Icon.FiEdit2}>
|
StartIcon={Icon.FiEdit2}>
|
||||||
{t("edit_team")}
|
{t("edit_team")}
|
||||||
@@ -176,7 +173,6 @@ export default function TeamListItem(props: Props) {
|
|||||||
<a target="_blank">
|
<a target="_blank">
|
||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
size="sm"
|
|
||||||
className="w-full rounded-none font-medium"
|
className="w-full rounded-none font-medium"
|
||||||
StartIcon={Icon.FiExternalLink}>
|
StartIcon={Icon.FiExternalLink}>
|
||||||
{t("preview_team")}
|
{t("preview_team")}
|
||||||
@@ -193,8 +189,7 @@ export default function TeamListItem(props: Props) {
|
|||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
color="warn"
|
color="destructive"
|
||||||
size="sm"
|
|
||||||
className="w-full rounded-none font-medium"
|
className="w-full rounded-none font-medium"
|
||||||
StartIcon={Icon.FiTrash}>
|
StartIcon={Icon.FiTrash}>
|
||||||
{t("disband_team")}
|
{t("disband_team")}
|
||||||
@@ -220,7 +215,7 @@ export default function TeamListItem(props: Props) {
|
|||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
color="warn"
|
color="destructive"
|
||||||
size="lg"
|
size="lg"
|
||||||
StartIcon={Icon.FiLogOut}
|
StartIcon={Icon.FiLogOut}
|
||||||
className="w-full rounded-none"
|
className="w-full rounded-none"
|
||||||
|
|||||||
@@ -163,7 +163,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
logo={item.integration.logo}
|
logo={item.integration.logo}
|
||||||
description={item.primary?.externalId || "No external Id"}
|
description={item.primary?.externalId || "No external Id"}
|
||||||
actions={
|
actions={
|
||||||
<div className="w-32">
|
<div className="flex w-32 justify-end">
|
||||||
<DisconnectIntegration
|
<DisconnectIntegration
|
||||||
credentialId={item.credentialId}
|
credentialId={item.credentialId}
|
||||||
trashIcon
|
trashIcon
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
||||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||||
|
const [seatsInputVisible, setSeatsInputVisible] = useState(!!eventType.seatsPerTimeSlot);
|
||||||
|
const [seatsPerTimeSlot, setSeatsPerTimeSlot] = useState(eventType.seatsPerTimeSlot);
|
||||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
||||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||||
);
|
);
|
||||||
@@ -293,6 +295,46 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<hr />
|
||||||
|
<Controller
|
||||||
|
name="seatsPerTimeSlot"
|
||||||
|
control={formMethods.control}
|
||||||
|
defaultValue={seatsPerTimeSlot}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<>
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
<Switch
|
||||||
|
name="seatsPerTimeSlot"
|
||||||
|
checked={seatsInputVisible}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
setSeatsInputVisible(e);
|
||||||
|
onChange(e ? seatsPerTimeSlot : null);
|
||||||
|
}}
|
||||||
|
fitToHeight={true}
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<Label className="text-sm font-semibold leading-none text-black">{t("offer_seats")}</Label>
|
||||||
|
<p className="-mt-2 text-sm leading-normal text-gray-600">{t("offer_seats_description")}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{seatsInputVisible && (
|
||||||
|
<div className="">
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
name="seatsPerTimeSlot"
|
||||||
|
label={t("number_of_seats")}
|
||||||
|
type="number"
|
||||||
|
defaultValue={seatsPerTimeSlot || 2}
|
||||||
|
addOnSuffix={<>{t("seats")}</>}
|
||||||
|
onChange={(e) => {
|
||||||
|
formMethods.setValue("seatsPerTimeSlot", Number(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
{showEventNameTip && (
|
{showEventNameTip && (
|
||||||
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
|
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
|
|||||||
@@ -27,13 +27,13 @@ const AppCard = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 rounded-md border border-gray-200 p-8">
|
<div className="mb-4 rounded-md border border-gray-200 p-8">
|
||||||
<div className="mb-6 flex w-full">
|
<div className="flex w-full">
|
||||||
<img src={logo} alt={name} className="mr-3 h-auto w-[42px] rounded-sm" />
|
<img src={logo} alt={name} className="mr-3 h-auto w-[42px] rounded-sm" />
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-semibold leading-none text-black">{name}</span>
|
<span className="font-semibold leading-none text-black">{name}</span>
|
||||||
<p className="pt-2 text-sm font-normal text-gray-600">{description}</p>
|
<p className="pt-2 text-sm font-normal text-gray-600">{description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto">
|
<div className="ml-auto flex items-center">
|
||||||
<Switch onCheckedChange={switchOnClick} checked={switchChecked} />
|
<Switch onCheckedChange={switchOnClick} checked={switchChecked} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ function SkeletonLoader() {
|
|||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
<div className="mb-4 flex items-center">
|
<div className="mb-4 flex items-center">
|
||||||
<SkeletonAvatar className="h-8 w-8" />
|
<SkeletonAvatar className="h-8 w-8" />
|
||||||
<div className="space-y-1">
|
<div className="flex flex-col space-y-1">
|
||||||
<SkeletonText className="h-4 w-16" />
|
<SkeletonText className="h-4 w-16" />
|
||||||
<SkeletonText className="h-4 w-24" />
|
<SkeletonText className="h-4 w-24" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const V2_WHITELIST = [
|
|||||||
"/event-types",
|
"/event-types",
|
||||||
"/workflows",
|
"/workflows",
|
||||||
"/apps",
|
"/apps",
|
||||||
|
"/teams",
|
||||||
"/success",
|
"/success",
|
||||||
"/auth/login",
|
"/auth/login",
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.0.0",
|
"version": "2.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ const providers: Provider[] = [
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
// ImpersonationProvider,
|
ImpersonationProvider,
|
||||||
];
|
];
|
||||||
|
|
||||||
if (IS_GOOGLE_LOGIN_ENABLED) {
|
if (IS_GOOGLE_LOGIN_ENABLED) {
|
||||||
|
|||||||
@@ -338,15 +338,18 @@ async function handler(req: NextApiRequest) {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Add an if conditional if there are no seats on the event type
|
||||||
// Assign to only one user when ROUND_ROBIN
|
// Assign to only one user when ROUND_ROBIN
|
||||||
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
|
if (!eventType.seatsPerTimeSlot) {
|
||||||
users = [await getLuckyUser("MAXIMIZE_AVAILABILITY", { availableUsers, eventTypeId: eventType.id })];
|
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
|
||||||
} else {
|
users = [await getLuckyUser("MAXIMIZE_AVAILABILITY", { availableUsers, eventTypeId: eventType.id })];
|
||||||
// excluding ROUND_ROBIN, all users have availability required.
|
} else {
|
||||||
if (availableUsers.length !== users.length) {
|
// excluding ROUND_ROBIN, all users have availability required.
|
||||||
throw new Error("Some users are unavailable for booking.");
|
if (availableUsers.length !== users.length) {
|
||||||
|
throw new Error("Some users are unavailable for booking.");
|
||||||
|
}
|
||||||
|
users = availableUsers;
|
||||||
}
|
}
|
||||||
users = availableUsers;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("available users", users);
|
console.log("available users", users);
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import UserV2OptInBanner from "@calcom/features/users/components/UserV2OptInBanner";
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { MobileNavigationMoreItems } from "@calcom/ui/Shell";
|
import { MobileNavigationMoreItems } from "@calcom/ui/Shell";
|
||||||
import { Shell } from "@calcom/ui/v2";
|
import { Shell } from "@calcom/ui/v2";
|
||||||
|
|||||||
@@ -100,7 +100,7 @@ const IntegrationsList = ({ data }: { data: inferQueryOutput<"viewer.integration
|
|||||||
logo={item.logo}
|
logo={item.logo}
|
||||||
description={item.description}
|
description={item.description}
|
||||||
actions={
|
actions={
|
||||||
<div className="flex w-32 justify-center">
|
<div className="flex w-32 justify-end">
|
||||||
<ConnectOrDisconnectIntegrationButton
|
<ConnectOrDisconnectIntegrationButton
|
||||||
credentialIds={item.credentialIds}
|
credentialIds={item.credentialIds}
|
||||||
type={item.type}
|
type={item.type}
|
||||||
|
|||||||
@@ -140,7 +140,7 @@ export default function Login({
|
|||||||
{...form.register("email")}
|
{...form.register("email")}
|
||||||
/>
|
/>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<div className="absolute right-0 -top-[6px]">
|
<div className="absolute right-0 -top-[6px] z-10">
|
||||||
<Link href="/auth/forgot-password">
|
<Link href="/auth/forgot-password">
|
||||||
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
|
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
|
||||||
{t("forgot")}
|
{t("forgot")}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const AppearanceView = () => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button color="primary" className="mt-8">
|
<Button type="submit" color="primary" className="mt-8">
|
||||||
{t("update")}
|
{t("update")}
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -188,7 +188,7 @@ const GeneralView = ({ localeProp }: GeneralViewProps) => {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<Button color="primary" className="mt-8">
|
<Button color="primary" type="submit" className="mt-8">
|
||||||
<>{t("update")}</>
|
<>{t("update")}</>
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useRef, useState, BaseSyntheticEvent } from "react";
|
|||||||
import { Controller, useForm } from "react-hook-form";
|
import { Controller, useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -58,6 +59,8 @@ const ProfileView = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const [confirmPasswordOpen, setConfirmPasswordOpen] = useState(false);
|
||||||
|
const [confirmPasswordErrorMessage, setConfirmPasswordDeleteErrorMessage] = useState("");
|
||||||
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
|
||||||
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
|
const [hasDeleteErrors, setHasDeleteErrors] = useState(false);
|
||||||
const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
|
const [deleteErrorMessage, setDeleteErrorMessage] = useState("");
|
||||||
@@ -81,6 +84,16 @@ const ProfileView = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const confirmPasswordMutation = trpc.useMutation("viewer.auth.verifyPassword", {
|
||||||
|
onSuccess() {
|
||||||
|
mutation.mutate(formMethods.getValues());
|
||||||
|
setConfirmPasswordOpen(false);
|
||||||
|
},
|
||||||
|
onError() {
|
||||||
|
setConfirmPasswordDeleteErrorMessage(t("incorrect_password"));
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const onDeleteMeErrorMutation = (error: TRPCClientErrorLike<AppRouter>) => {
|
const onDeleteMeErrorMutation = (error: TRPCClientErrorLike<AppRouter>) => {
|
||||||
setHasDeleteErrors(true);
|
setHasDeleteErrors(true);
|
||||||
setDeleteErrorMessage(errorMessages[error.message]);
|
setDeleteErrorMessage(errorMessages[error.message]);
|
||||||
@@ -93,6 +106,12 @@ const ProfileView = () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onConfirmPassword = (e: Event | React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const password = passwordRef.current.value;
|
||||||
|
confirmPasswordMutation.mutate({ passwordInput: password });
|
||||||
|
};
|
||||||
|
|
||||||
const onConfirmButton = (e: Event | React.MouseEvent<HTMLElement, MouseEvent>) => {
|
const onConfirmButton = (e: Event | React.MouseEvent<HTMLElement, MouseEvent>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const totpCode = form.getValues("totpCode");
|
const totpCode = form.getValues("totpCode");
|
||||||
@@ -134,7 +153,11 @@ const ProfileView = () => {
|
|||||||
<Form
|
<Form
|
||||||
form={formMethods}
|
form={formMethods}
|
||||||
handleSubmit={(values) => {
|
handleSubmit={(values) => {
|
||||||
mutation.mutate(values);
|
if (values.email !== user?.email) {
|
||||||
|
setConfirmPasswordOpen(true);
|
||||||
|
} else {
|
||||||
|
mutation.mutate(values);
|
||||||
|
}
|
||||||
}}>
|
}}>
|
||||||
<Meta title="Profile" description="Manage settings for your cal profile" />
|
<Meta title="Profile" description="Manage settings for your cal profile" />
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
@@ -159,73 +182,24 @@ const ProfileView = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
<div className="mt-8">
|
||||||
control={formMethods.control}
|
<TextField
|
||||||
name="username"
|
data-testid="username-input"
|
||||||
render={({ field: { value } }) => (
|
label={t("personal_cal_url")}
|
||||||
<div className="mt-8">
|
addOnLeading={WEBSITE_URL + "/"}
|
||||||
<TextField
|
{...formMethods.register("username")}
|
||||||
data-testid="username-input"
|
/>
|
||||||
name="username"
|
</div>
|
||||||
label={t("personal_cal_url")}
|
<div className="mt-8">
|
||||||
addOnLeading="https://cal.com/"
|
<TextField label={t("full_name")} {...formMethods.register("name")} />
|
||||||
value={value}
|
</div>
|
||||||
onChange={(e) => {
|
<div className="mt-8">
|
||||||
formMethods.setValue("username", e?.target.value);
|
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
|
||||||
}}
|
</div>
|
||||||
/>
|
<div className="mt-8">
|
||||||
</div>
|
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
|
||||||
)}
|
</div>
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={formMethods.control}
|
|
||||||
name="name"
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<div className="mt-8">
|
|
||||||
<TextField
|
|
||||||
label={t("full_name")}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange(e?.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={formMethods.control}
|
|
||||||
name="email"
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<div className="mt-8">
|
|
||||||
<TextField
|
|
||||||
name="email"
|
|
||||||
label={t("email")}
|
|
||||||
value={value}
|
|
||||||
hint={t("change_email_hint")}
|
|
||||||
onChange={(e) => {
|
|
||||||
formMethods.setValue("email", e?.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Controller
|
|
||||||
control={formMethods.control}
|
|
||||||
name="bio"
|
|
||||||
render={({ field: { value } }) => (
|
|
||||||
<div className="mt-8">
|
|
||||||
<TextField
|
|
||||||
name="bio"
|
|
||||||
label={t("about")}
|
|
||||||
hint={t("bio_hint")}
|
|
||||||
value={value}
|
|
||||||
onChange={(e) => {
|
|
||||||
formMethods.setValue("bio", e?.target.value);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<Button color="primary" className="mt-8" type="submit" loading={mutation.isLoading}>
|
<Button color="primary" className="mt-8" type="submit" loading={mutation.isLoading}>
|
||||||
{t("update")}
|
{t("update")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -279,6 +253,32 @@ const ProfileView = () => {
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
{/* If changing email, confirm password */}
|
||||||
|
<Dialog open={confirmPasswordOpen} onOpenChange={setConfirmPasswordOpen}>
|
||||||
|
<DialogContent
|
||||||
|
title={t("confirm_password")}
|
||||||
|
description={t("confirm_password_change_email")}
|
||||||
|
type="creation"
|
||||||
|
actionText={t("confirm")}
|
||||||
|
Icon={Icon.FiAlertTriangle}
|
||||||
|
actionOnClick={(e) => e && onConfirmPassword(e)}>
|
||||||
|
<>
|
||||||
|
<PasswordField
|
||||||
|
data-testid="password"
|
||||||
|
name="password"
|
||||||
|
id="password"
|
||||||
|
type="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
label="Password"
|
||||||
|
ref={passwordRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{confirmPasswordErrorMessage && <Alert severity="error" title={confirmPasswordErrorMessage} />}
|
||||||
|
</>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
import { Shell } from "@calcom/ui/v2";
|
||||||
|
import { Alert } from "@calcom/ui/v2/core/Alert";
|
||||||
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
||||||
|
|
||||||
|
import SkeletonLoaderTeamList from "@components/team/SkeletonloaderTeamList";
|
||||||
|
import TeamCreateModal from "@components/team/TeamCreateModal";
|
||||||
|
import TeamList from "@components/team/TeamList";
|
||||||
|
|
||||||
|
function Teams() {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const [showCreateTeamModal, setShowCreateTeamModal] = useState(false);
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
|
||||||
|
const { data, isLoading } = trpc.useQuery(["viewer.teams.list"], {
|
||||||
|
onError: (e) => {
|
||||||
|
setErrorMessage(e.message);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const teams = data?.filter((m) => m.accepted) || [];
|
||||||
|
const invites = data?.filter((m) => !m.accepted) || [];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Shell
|
||||||
|
heading={t("teams")}
|
||||||
|
subtitle={t("create_manage_teams_collaborative")}
|
||||||
|
CTA={
|
||||||
|
<Button type="button" onClick={() => setShowCreateTeamModal(true)}>
|
||||||
|
<Icon.FiPlus className="inline-block h-3.5 w-3.5 text-white group-hover:text-black ltr:mr-2 rtl:ml-2" />
|
||||||
|
{t("new")}
|
||||||
|
</Button>
|
||||||
|
}>
|
||||||
|
<>
|
||||||
|
{!!errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||||
|
{showCreateTeamModal && (
|
||||||
|
<TeamCreateModal isOpen={showCreateTeamModal} onClose={() => setShowCreateTeamModal(false)} />
|
||||||
|
)}
|
||||||
|
{invites.length > 0 && (
|
||||||
|
<div className="mb-4">
|
||||||
|
<h1 className="mb-2 text-lg font-medium">{t("open_invitations")}</h1>
|
||||||
|
<TeamList teams={invites} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isLoading && <SkeletonLoaderTeamList />}
|
||||||
|
{!teams.length && !isLoading && (
|
||||||
|
<EmptyScreen
|
||||||
|
Icon={Icon.FiUsers}
|
||||||
|
headline={t("no_teams")}
|
||||||
|
description={t("no_teams_description")}
|
||||||
|
buttonRaw={
|
||||||
|
<Button color="secondary" onClick={() => setShowCreateTeamModal(true)}>
|
||||||
|
{t("create_team")}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
buttonOnClick={() => setShowCreateTeamModal(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{teams.length > 0 && <TeamList teams={teams} />}
|
||||||
|
</>
|
||||||
|
</Shell>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Teams.requiresLicense = false;
|
||||||
|
|
||||||
|
export default Teams;
|
||||||
@@ -843,7 +843,7 @@
|
|||||||
"redirect_url_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
|
"redirect_url_upgrade_description": "In order to use this feature, you need to upgrade to a Pro account.",
|
||||||
"duplicate": "Duplicate",
|
"duplicate": "Duplicate",
|
||||||
"offer_seats": "Offer seats",
|
"offer_seats": "Offer seats",
|
||||||
"offer_seats_description": "Offer seats to bookings (This disables guests & opt in bookings)",
|
"offer_seats_description": "Offer seats for booking. This automatically disables guest & opt-in bookings.",
|
||||||
"seats_available": "Seats available",
|
"seats_available": "Seats available",
|
||||||
"number_of_seats": "Number of seats per booking",
|
"number_of_seats": "Number of seats per booking",
|
||||||
"enter_number_of_seats": "Enter number of seats",
|
"enter_number_of_seats": "Enter number of seats",
|
||||||
@@ -1265,5 +1265,7 @@
|
|||||||
"no_calendar_installed": "No calendar installed",
|
"no_calendar_installed": "No calendar installed",
|
||||||
"no_calendar_installed_description": "You have not yet connected any of your calendars",
|
"no_calendar_installed_description": "You have not yet connected any of your calendars",
|
||||||
"add_a_calendar": "Add a calendar",
|
"add_a_calendar": "Add a calendar",
|
||||||
"change_email_hint": "You may need to log out and back in to see any change take effect"
|
"change_email_hint": "You may need to log out and back in to see any change take effect",
|
||||||
|
"confirm_password_change_email": "Please confirm your password before changing your email address",
|
||||||
|
"seats": "seats"
|
||||||
}
|
}
|
||||||
+1
-1
Submodule apps/website updated: b9d73fd609...0ebd35d880.
+1
-1
@@ -49,7 +49,7 @@ const TextWidget = (props: TextWidgetProps & { type?: string }) => {
|
|||||||
const textValue = value || "";
|
const textValue = value || "";
|
||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
containerClassName="w-full"
|
containerClassName="w-full mt-2"
|
||||||
type={type}
|
type={type}
|
||||||
className="dark:border-darkgray-300 flex flex-grow border-gray-300 text-sm dark:bg-transparent dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500"
|
className="dark:border-darkgray-300 flex flex-grow border-gray-300 text-sm dark:bg-transparent dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500"
|
||||||
value={textValue}
|
value={textValue}
|
||||||
|
|||||||
@@ -243,8 +243,10 @@ const Route = ({
|
|||||||
<TextField
|
<TextField
|
||||||
name="externalRedirectUrl"
|
name="externalRedirectUrl"
|
||||||
className="flex w-full flex-grow border-gray-300 text-sm"
|
className="flex w-full flex-grow border-gray-300 text-sm"
|
||||||
|
containerClassName="w-full mt-2"
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
|
labelSrOnly
|
||||||
value={route.action.value}
|
value={route.action.value}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
|
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
import CredentialsProvider from "next-auth/providers/credentials";
|
import CredentialsProvider from "next-auth/providers/credentials";
|
||||||
import { getSession } from "next-auth/react";
|
import { getSession } from "next-auth/react";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { asNumberOrThrow } from "@lib/asStringOrNull";
|
const teamIdschema = z.object({
|
||||||
|
teamId: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
const auditAndReturnNextUser = async (
|
const auditAndReturnNextUser = async (
|
||||||
impersonatedUser: Pick<User, "id" | "username" | "email" | "name" | "role">,
|
impersonatedUser: Pick<User, "id" | "username" | "email" | "name" | "role">,
|
||||||
@@ -50,7 +53,8 @@ const ImpersonationProvider = CredentialsProvider({
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
// @ts-ignore need to figure out how to correctly type this
|
// @ts-ignore need to figure out how to correctly type this
|
||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
const teamId = creds?.teamId ? asNumberOrThrow(creds.teamId) : undefined;
|
// If teamId is present -> parse the teamId and throw error itn ot number. If not present teamId is set to undefined
|
||||||
|
const teamId = creds?.teamId ? teamIdschema.parse(creds).teamId : undefined;
|
||||||
|
|
||||||
if (session?.user.username === creds?.username) {
|
if (session?.user.username === creds?.username) {
|
||||||
throw new Error("You cannot impersonate yourself.");
|
throw new Error("You cannot impersonate yourself.");
|
||||||
@@ -102,6 +106,8 @@ const ImpersonationProvider = CredentialsProvider({
|
|||||||
return auditAndReturnNextUser(impersonatedUser, session?.user.id as number);
|
return auditAndReturnNextUser(impersonatedUser, session?.user.id as number);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!teamId) throw new Error("You do not have permission to do this.");
|
||||||
|
|
||||||
// Check session
|
// Check session
|
||||||
const sessionUserFromDb = await prisma.user.findUnique({
|
const sessionUserFromDb = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ function WorkflowsPage() {
|
|||||||
const session = useSession();
|
const session = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const me = useMeQuery();
|
|
||||||
const isFreeUser = me.data?.plan === "FREE";
|
|
||||||
|
|
||||||
const { data, isLoading } = trpc.useQuery(["viewer.workflows.list"]);
|
const { data, isLoading } = trpc.useQuery(["viewer.workflows.list"]);
|
||||||
|
|
||||||
const createMutation = trpc.useMutation("viewer.workflows.createV2", {
|
const createMutation = trpc.useMutation("viewer.workflows.createV2", {
|
||||||
@@ -47,7 +44,7 @@ function WorkflowsPage() {
|
|||||||
title={t("workflows")}
|
title={t("workflows")}
|
||||||
subtitle={data?.workflows.length ? t("workflows_to_automate_notifications") : ""}
|
subtitle={data?.workflows.length ? t("workflows_to_automate_notifications") : ""}
|
||||||
CTA={
|
CTA={
|
||||||
session.data?.hasValidLicense && !isFreeUser && data?.workflows && data?.workflows.length > 0 ? (
|
session.data?.hasValidLicense && data?.workflows && data?.workflows.length > 0 ? (
|
||||||
<Button
|
<Button
|
||||||
StartIcon={Icon.FiPlus}
|
StartIcon={Icon.FiPlus}
|
||||||
onClick={() => createMutation.mutate()}
|
onClick={() => createMutation.mutate()}
|
||||||
@@ -63,11 +60,7 @@ function WorkflowsPage() {
|
|||||||
<SkeletonLoader />
|
<SkeletonLoader />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
{isFreeUser ? (
|
<WorkflowList workflows={data?.workflows} />
|
||||||
<Alert className="border " severity="warning" title={t("pro_feature_workflows")} />
|
|
||||||
) : (
|
|
||||||
<WorkflowList workflows={data?.workflows} />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</LicenseRequired>
|
</LicenseRequired>
|
||||||
|
|||||||
@@ -69,8 +69,6 @@ function WorkflowPage() {
|
|||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const me = useMeQuery();
|
|
||||||
const isFreeUser = me.data?.plan === "FREE";
|
|
||||||
|
|
||||||
const [selectedEventTypes, setSelectedEventTypes] = useState<Option[]>([]);
|
const [selectedEventTypes, setSelectedEventTypes] = useState<Option[]>([]);
|
||||||
const [isAllDataLoaded, setIsAllDataLoaded] = useState(false);
|
const [isAllDataLoaded, setIsAllDataLoaded] = useState(false);
|
||||||
@@ -189,45 +187,36 @@ function WorkflowPage() {
|
|||||||
<Shell
|
<Shell
|
||||||
title={workflow && workflow.name ? workflow.name : "Untitled"}
|
title={workflow && workflow.name ? workflow.name : "Untitled"}
|
||||||
CTA={
|
CTA={
|
||||||
!isFreeUser && (
|
<div>
|
||||||
<div>
|
<Button type="submit">{t("save")}</Button>
|
||||||
<Button type="submit">{t("save")}</Button>
|
</div>
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
heading={
|
heading={
|
||||||
session.data?.hasValidLicense &&
|
session.data?.hasValidLicense &&
|
||||||
isAllDataLoaded &&
|
isAllDataLoaded && (
|
||||||
!isFreeUser && (
|
|
||||||
<div className={classNames(workflow && !workflow.name ? "text-gray-400" : "")}>
|
<div className={classNames(workflow && !workflow.name ? "text-gray-400" : "")}>
|
||||||
{workflow && workflow.name ? workflow.name : "untitled"}
|
{workflow && workflow.name ? workflow.name : "untitled"}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}>
|
}>
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
{isFreeUser ? (
|
{!isError ? (
|
||||||
<Alert className="border " severity="warning" title={t("pro_feature_workflows")} />
|
|
||||||
) : (
|
|
||||||
<>
|
<>
|
||||||
{!isError ? (
|
{isAllDataLoaded ? (
|
||||||
<>
|
<>
|
||||||
{isAllDataLoaded ? (
|
<WorkflowDetailsPage
|
||||||
<>
|
form={form}
|
||||||
<WorkflowDetailsPage
|
workflowId={+workflowId}
|
||||||
form={form}
|
selectedEventTypes={selectedEventTypes}
|
||||||
workflowId={+workflowId}
|
setSelectedEventTypes={setSelectedEventTypes}
|
||||||
selectedEventTypes={selectedEventTypes}
|
/>
|
||||||
setSelectedEventTypes={setSelectedEventTypes}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<SkeletonLoader />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Alert severity="error" title="Something went wrong" message={error.message} />
|
<SkeletonLoader />
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
) : (
|
||||||
|
<Alert severity="error" title="Something went wrong" message={error.message} />
|
||||||
)}
|
)}
|
||||||
</LicenseRequired>
|
</LicenseRequired>
|
||||||
</Shell>
|
</Shell>
|
||||||
|
|||||||
+4
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
-- Every user is PRO now, later this column will be deprecated.
|
||||||
|
UPDATE "users" SET "plan" = 'PRO';
|
||||||
|
ALTER TABLE "users" ALTER COLUMN "plan" SET DEFAULT 'PRO';
|
||||||
@@ -163,7 +163,7 @@ model User {
|
|||||||
identityProviderId String?
|
identityProviderId String?
|
||||||
availability Availability[]
|
availability Availability[]
|
||||||
invitedTo Int?
|
invitedTo Int?
|
||||||
plan UserPlan @default(FREE)
|
plan UserPlan @default(PRO)
|
||||||
webhooks Webhook[]
|
webhooks Webhook[]
|
||||||
brandColor String @default("#292929")
|
brandColor String @default("#292929")
|
||||||
darkBrandColor String @default("#fafafa")
|
darkBrandColor String @default("#fafafa")
|
||||||
|
|||||||
@@ -8,56 +8,81 @@ import { TRPCError } from "@trpc/server";
|
|||||||
|
|
||||||
import { createProtectedRouter } from "../../createRouter";
|
import { createProtectedRouter } from "../../createRouter";
|
||||||
|
|
||||||
export const authRouter = createProtectedRouter().mutation("changePassword", {
|
export const authRouter = createProtectedRouter()
|
||||||
input: z.object({
|
.mutation("changePassword", {
|
||||||
oldPassword: z.string(),
|
input: z.object({
|
||||||
newPassword: z.string(),
|
oldPassword: z.string(),
|
||||||
}),
|
newPassword: z.string(),
|
||||||
async resolve({ input, ctx }) {
|
}),
|
||||||
const { oldPassword, newPassword } = input;
|
async resolve({ input, ctx }) {
|
||||||
|
const { oldPassword, newPassword } = input;
|
||||||
|
|
||||||
const { user } = ctx;
|
const { user } = ctx;
|
||||||
|
|
||||||
if (user.identityProvider !== IdentityProvider.CAL) {
|
if (user.identityProvider !== IdentityProvider.CAL) {
|
||||||
throw new TRPCError({ code: "FORBIDDEN", message: "THIRD_PARTY_IDENTITY_PROVIDER_ENABLED" });
|
throw new TRPCError({ code: "FORBIDDEN", message: "THIRD_PARTY_IDENTITY_PROVIDER_ENABLED" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPasswordQuery = await prisma.user.findFirst({
|
const currentPasswordQuery = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
password: true,
|
password: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const currentPassword = currentPasswordQuery?.password;
|
const currentPassword = currentPasswordQuery?.password;
|
||||||
|
|
||||||
if (!currentPassword) {
|
if (!currentPassword) {
|
||||||
throw new TRPCError({ code: "NOT_FOUND", message: "MISSING_PASSWORD" });
|
throw new TRPCError({ code: "NOT_FOUND", message: "MISSING_PASSWORD" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
|
const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
|
||||||
if (!passwordsMatch) {
|
if (!passwordsMatch) {
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "INCORRECT_PASSWORD" });
|
throw new TRPCError({ code: "BAD_REQUEST", message: "INCORRECT_PASSWORD" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (oldPassword === newPassword) {
|
if (oldPassword === newPassword) {
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "PASSWORD_MATCHES_OLD" });
|
throw new TRPCError({ code: "BAD_REQUEST", message: "PASSWORD_MATCHES_OLD" });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!validPassword(newPassword)) {
|
if (!validPassword(newPassword)) {
|
||||||
throw new TRPCError({ code: "BAD_REQUEST", message: "INVALID_PASSWORD" });
|
throw new TRPCError({ code: "BAD_REQUEST", message: "INVALID_PASSWORD" });
|
||||||
}
|
}
|
||||||
|
|
||||||
const hashedPassword = await hashPassword(newPassword);
|
const hashedPassword = await hashPassword(newPassword);
|
||||||
await prisma.user.update({
|
await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: user.id,
|
id: user.id,
|
||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
password: hashedPassword,
|
password: hashedPassword,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
.mutation("verifyPassword", {
|
||||||
|
input: z.object({
|
||||||
|
passwordInput: z.string(),
|
||||||
|
}),
|
||||||
|
async resolve({ input, ctx }) {
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: {
|
||||||
|
id: ctx.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user?.password) {
|
||||||
|
throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordsMatch = await verifyPassword(input.passwordInput, user.password);
|
||||||
|
|
||||||
|
if (!passwordsMatch) {
|
||||||
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -382,6 +382,8 @@ export type NavigationItemType = {
|
|||||||
icon?: SVGComponent;
|
icon?: SVGComponent;
|
||||||
child?: NavigationItemType[];
|
child?: NavigationItemType[];
|
||||||
pro?: true;
|
pro?: true;
|
||||||
|
onlyMobile?: boolean;
|
||||||
|
onlyDesktop?: boolean;
|
||||||
isCurrent?: ({
|
isCurrent?: ({
|
||||||
item,
|
item,
|
||||||
isChild,
|
isChild,
|
||||||
@@ -411,6 +413,12 @@ const navigation: NavigationItemType[] = [
|
|||||||
href: "/availability",
|
href: "/availability",
|
||||||
icon: Icon.FiClock,
|
icon: Icon.FiClock,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "teams",
|
||||||
|
href: "/teams",
|
||||||
|
icon: Icon.FiUsers,
|
||||||
|
onlyDesktop: true,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "apps",
|
name: "apps",
|
||||||
href: "/apps",
|
href: "/apps",
|
||||||
@@ -480,7 +488,7 @@ const { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMor
|
|||||||
// We filter out the "more" separator in desktop navigation
|
// We filter out the "more" separator in desktop navigation
|
||||||
if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item);
|
if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item);
|
||||||
// Items for mobile bottom navigation
|
// Items for mobile bottom navigation
|
||||||
if (index < moreSeparatorIndex + 1) items.mobileNavigationBottomItems.push(item);
|
if (index < moreSeparatorIndex + 1 && !item.onlyDesktop) items.mobileNavigationBottomItems.push(item);
|
||||||
// Items for the "more" menu in mobile navigation
|
// Items for the "more" menu in mobile navigation
|
||||||
else items.mobileNavigationMoreItems.push(item);
|
else items.mobileNavigationMoreItems.push(item);
|
||||||
return items;
|
return items;
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default function AllApps({ apps }: AllAppsPropsType) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-16">
|
<div className="mb-16">
|
||||||
<div className="mb-4 flex flex-col justify-between lg:flex-row lg:items-center">
|
<div className="relative mb-4 flex flex-col justify-between lg:flex-row lg:items-center">
|
||||||
<h2 className="text-lg font-semibold text-gray-900 ">
|
<h2 className="text-lg font-semibold text-gray-900 ">
|
||||||
{t("explore_apps", {
|
{t("explore_apps", {
|
||||||
category:
|
category:
|
||||||
@@ -70,7 +70,7 @@ export default function AllApps({ apps }: AllAppsPropsType) {
|
|||||||
})}
|
})}
|
||||||
</h2>
|
</h2>
|
||||||
{leftVisible && (
|
{leftVisible && (
|
||||||
<div className="absolute left-1/2 flex">
|
<div className="absolute top-9 flex md:left-1/2 md:-top-1">
|
||||||
<div className="flex h-12 w-5 items-center justify-end bg-white">
|
<div className="flex h-12 w-5 items-center justify-end bg-white">
|
||||||
<ChevronLeft className="h-4 w-4 text-gray-500" />
|
<ChevronLeft className="h-4 w-4 text-gray-500" />
|
||||||
</div>
|
</div>
|
||||||
@@ -112,7 +112,7 @@ export default function AllApps({ apps }: AllAppsPropsType) {
|
|||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
{rightVisible && (
|
{rightVisible && (
|
||||||
<div className="absolute right-12 flex">
|
<div className="absolute top-9 right-0 flex md:-top-1">
|
||||||
<div className="flex h-12 w-5 bg-gradient-to-r from-transparent to-white" />
|
<div className="flex h-12 w-5 bg-gradient-to-r from-transparent to-white" />
|
||||||
<div className="flex h-12 w-5 items-center justify-end bg-white">
|
<div className="flex h-12 w-5 items-center justify-end bg-white">
|
||||||
<ChevronRight className="h-4 w-4 text-gray-500" />
|
<ChevronRight className="h-4 w-4 text-gray-500" />
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(funct
|
|||||||
|
|
||||||
<Tooltip content={textLabel}>
|
<Tooltip content={textLabel}>
|
||||||
<button
|
<button
|
||||||
className="absolute bottom-0 right-3 h-9 text-gray-900"
|
className="absolute bottom-2 right-3 h-9 text-gray-900"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => toggleIsPasswordVisible()}>
|
onClick={() => toggleIsPasswordVisible()}>
|
||||||
{isPasswordVisible ? (
|
{isPasswordVisible ? (
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export default function InstalledAppsLayout({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Shell {...rest}>
|
<Shell {...rest}>
|
||||||
<div className="mt-10 flex flex-col p-2 md:p-0 xl:flex-row">
|
<div className="flex flex-col p-2 md:p-0 xl:flex-row">
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block">
|
||||||
<VerticalTabs tabs={actualTabs} sticky />
|
<VerticalTabs tabs={actualTabs} sticky />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -213,14 +213,14 @@ const SettingsSidebarContainer = ({ className = "" }) => {
|
|||||||
{team.accepted && (
|
{team.accepted && (
|
||||||
<VerticalTabItem
|
<VerticalTabItem
|
||||||
name={t("profile")}
|
name={t("profile")}
|
||||||
href={`${WEBAPP_URL}/settings/teams/${team.id}/profile`}
|
href={`/settings/teams/${team.id}/profile`}
|
||||||
textClassNames="px-3 text-gray-900 font-medium text-sm"
|
textClassNames="px-3 text-gray-900 font-medium text-sm"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<VerticalTabItem
|
<VerticalTabItem
|
||||||
name={t("members")}
|
name={t("members")}
|
||||||
href={`${WEBAPP_URL}/settings/teams/${team.id}/members`}
|
href={`/settings/teams/${team.id}/members`}
|
||||||
textClassNames="px-3 text-gray-900 font-medium text-sm"
|
textClassNames="px-3 text-gray-900 font-medium text-sm"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
@@ -235,7 +235,7 @@ const SettingsSidebarContainer = ({ className = "" }) => {
|
|||||||
/> */}
|
/> */}
|
||||||
<VerticalTabItem
|
<VerticalTabItem
|
||||||
name={t("appearance")}
|
name={t("appearance")}
|
||||||
href={`${WEBAPP_URL}/settings/teams/${team.id}/appearance`}
|
href={`/settings/teams/${team.id}/appearance`}
|
||||||
textClassNames="px-3 text-gray-900 font-medium text-sm"
|
textClassNames="px-3 text-gray-900 font-medium text-sm"
|
||||||
disableChevron
|
disableChevron
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -153,7 +153,11 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
|
|||||||
) : (
|
) : (
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button EndIcon={Icon.FiChevronDown}>{t("new")}</Button>
|
<Button
|
||||||
|
EndIcon={Icon.FiChevronDown}
|
||||||
|
className="radix-state-open:bg-brand-500 radix-state-open:ring-2 radix-state-open:ring-brand-500 ring-offset-2">
|
||||||
|
{t("new")}
|
||||||
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>
|
<DropdownMenuLabel>
|
||||||
|
|||||||
Reference in new issue
Block a user