Compare commits

...
18 Commits
Author SHA1 Message Date
zomars 8c0e2b5129 v2.0.1 2022-09-17 23:29:10 +01:00
Joe Au-YeungandOmar López cf32287109 Add password on confirmation when changing email (#4503)
* Add email confirmation

* Confirm password only if email was changed

Co-authored-by: Omar López <zomars@me.com>
2022-09-17 23:27:25 +01:00
Leo GiovanettiandOmar López 2ef8e230c5 Categories strip fix (#4562)
Co-authored-by: Omar López <zomars@me.com>
2022-09-17 23:26:01 +01:00
546d0d50c4 Impersonation fix (#4521)
* Impersonation fix

* Update packages/features/ee/impersonation/lib/ImpersonationProvider.ts

Co-authored-by: Omar López <zomars@me.com>

* Fix zod schema

* Early returns

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
2022-09-17 21:09:06 +00:00
Joe Au-Yeung 33e8198779 Hotfix To Seats on Event Types (#4528)
* Hotfix

* Add seats input to event type page
2022-09-17 21:44:29 +01:00
Leo Giovanettiandzomars e7dd0ff3e9 Removing update table user plan (#4551)
* Removing update table user plan

* All users are PRO now

Co-authored-by: zomars <zomars@me.com>
2022-09-17 20:29:06 +00:00
Leo GiovanettiandPeer Richelsen 1b2707d4f0 Old teams page in v2 (#4553)
* Old teams page in v2

* Tea nav item fix

* Team creation modal converted to v2

* Updated emptyscreen and shell button

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-09-17 17:53:31 +00:00
Agusti Fernandez Pardo 00071bcdf0 fix: general settings (Save / Update on submit) (#4557) 2022-09-17 17:09:52 +00:00
Jonathan Giardino fc5cc5b8bc Fix: event types skeleton is off #4540 (#4550) 2022-09-17 13:46:11 +00:00
Mackenly Jones 1e79a55825 Update vercel.mdx (#4545) 2022-09-17 11:26:55 +00:00
Alex van Andel 4645bbcb75 Use refs for inputs, full_name label, un-hardcoded WEBSITE_URL (#4542) 2022-09-17 02:40:27 +00:00
95b903110f Fix alignment issues (#4495)
* fix: spacing issue in externalRedirectUrl text input

* fix: remove unnecessary margin from appcard

* fix: center align switch

* fix: alignment in widget text field

* Update packages/app-store/ee/routing_forms/pages/route-builder/[...appPages].tsx

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-09-17 02:00:22 +00:00
Leo Giovanetti 2917455324 Mobile tweaks app store (#4537) 2022-09-16 22:15:47 +00:00
Hariom BalharaandLeo Giovanetti d71765848c Fix workflows (#4518)
Co-authored-by: Leo Giovanetti <hello@leog.me>
2022-09-16 18:49:04 -03:00
Leo GiovanettiandBailey Pumfleet cf7b7d7d3d Fixing forget password link in login (#4526)
* Fixing forget password link in login

* Showing heap info

Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
2022-09-16 15:27:44 +00:00
sean-brydon 96aaa36751 Fix dropdown focus + public dark mode buttons (#4527)
* Fix dropdown focus

* Fix darkmode buttons to use brand
2022-09-16 14:29:40 +00:00
Hariom Balhara 52e74245a4 Settings View Fixes - Theme not working/Nav Items not highlighted (#4522)
* Fix theme not working

* Fix Nav items of teams not highlighted
2022-09-16 12:03:58 +00:00
Bailey Pumfleet cc03b132e6 Update submodules 2022-09-15 21:29:50 +00:00
38 changed files with 370 additions and 235 deletions
+7
View File
@@ -8,6 +8,8 @@ on:
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
env:
NODE_OPTIONS: "--max-old-space-size=8192"
jobs:
check-types:
runs-on: ubuntu-latest
@@ -20,8 +22,13 @@ jobs:
- name: Use Node 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
cache: "yarn"
- name: Show info
run: node -e "console.log(require('v8').getHeapStatistics())"
- run: yarn --frozen-lockfile
- run: yarn type-check
+1 -1
View File
@@ -6,7 +6,7 @@ title: Vercel
## 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
@@ -34,11 +34,11 @@ import useTheme from "@calcom/lib/hooks/useTheme";
import { HttpError } from "@calcom/lib/http-error";
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { Button } from "@calcom/ui/Button";
import { Icon } from "@calcom/ui/Icon";
import { Tooltip } from "@calcom/ui/Tooltip";
import PhoneInput from "@calcom/ui/form/PhoneInputLazy";
import { EmailInput, Form } from "@calcom/ui/form/fields";
import { Button } from "@calcom/ui/v2";
import { asStringOrNull } from "@lib/asStringOrNull";
import { timeZone } from "@lib/clock";
@@ -199,16 +199,12 @@ const BookingPage = ({
// There should only exists one default userData variable for primaryAttendee.
const defaultUserValues = {
email: booking?.attendees[0].email
? booking.attendees[0].email
email: rescheduleUid
? booking?.attendees[0].email
: router.query.email
? (router.query.email as string)
: "",
name: booking?.attendees[0].name
? booking.attendees[0].name
: router.query.name
? (router.query.name as string)
: "",
name: rescheduleUid ? booking?.attendees[0].name : router.query.name ? (router.query.name as string) : "",
};
const defaultValues = () => {
@@ -595,9 +591,7 @@ const BookingPage = ({
required
className={classNames(
inputClassName,
bookingForm.formState.errors.email
? "border-red-700 focus:ring-red-700"
: " border-gray-300 dark:border-gray-900"
bookingForm.formState.errors.email && "!focus:ring-red-700 !border-red-700"
)}
placeholder="you@example.com"
type="search" // Disables annoying 1password intrusive popup (non-optimal, I know I know...)
@@ -756,7 +750,9 @@ const BookingPage = ({
render={({ field: { onChange, value } }) => (
<ReactMultiEmail
className="relative"
placeholder="guest@example.com"
placeholder={
<span className="dark:text-darkgray-600">guest@example.com</span>
}
emails={value}
onChange={onChange}
getLabel={(
@@ -854,7 +850,7 @@ const BookingPage = ({
type="button"
color="secondary"
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" />
{t("additional_guests")}
</Button>
@@ -864,12 +860,12 @@ const BookingPage = ({
type="button"
onClick={() => router.back()}
// 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")}
</Button>
<Button
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"}
loading={mutation.isLoading || recurringMutation.isLoading}>
{rescheduleUid ? t("reschedule") : t("confirm")}
+5 -18
View File
@@ -3,9 +3,9 @@ import { useRef, useState } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button } from "@calcom/ui";
import { Alert } from "@calcom/ui/Alert";
import { Dialog, DialogContent, DialogFooter } from "@calcom/ui/Dialog";
import { 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 {
isOpen: boolean;
@@ -28,15 +28,14 @@ export default function TeamCreate(props: Props) {
},
});
const createTeam = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const createTeam = () => {
createTeamMutation.mutate({ name: nameRef?.current?.value });
};
return (
<>
<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="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" />
@@ -50,7 +49,7 @@ export default function TeamCreate(props: Props) {
</div>
</div>
</div>
<form onSubmit={createTeam}>
<form>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium text-gray-700">
{t("name")}
@@ -66,18 +65,6 @@ export default function TeamCreate(props: Props) {
/>
</div>
{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>
</DialogContent>
</Dialog>
+1 -1
View File
@@ -1,7 +1,7 @@
import { useState } from "react";
import showToast from "@calcom/lib/notification";
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
import showToast from "@calcom/ui/v2/core/notifications";
import TeamListItem from "./TeamListItem";
+12 -17
View File
@@ -4,19 +4,19 @@ import Link from "next/link";
import classNames from "@calcom/lib/classNames";
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
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 ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import { Icon } from "@calcom/ui/Icon";
import Button from "@calcom/ui/v2/core/Button";
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
import { Dialog, DialogTrigger } from "@calcom/ui/v2/core/Dialog";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@calcom/ui/Dropdown";
import { Icon } from "@calcom/ui/Icon";
import { Tooltip } from "@calcom/ui/Tooltip";
} from "@calcom/ui/v2/core/Dropdown";
import { Tooltip } from "@calcom/ui/v2/core/Tooltip";
import showToast from "@calcom/ui/v2/core/notifications";
import Avatar from "@components/ui/Avatar";
@@ -84,7 +84,7 @@ export default function TeamListItem(props: Props) {
!isInvitee && "group hover:bg-neutral-50"
)}>
{!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}`}>
{teamInfo}
</a>
@@ -112,7 +112,6 @@ export default function TeamListItem(props: Props) {
<DropdownMenuItem>
<Button
color="minimal"
size="sm"
className="w-full rounded-none font-medium"
StartIcon={Icon.FiCheck}
onClick={acceptInvite}>
@@ -121,8 +120,7 @@ export default function TeamListItem(props: Props) {
</DropdownMenuItem>
<DropdownMenuItem>
<Button
color="warn"
size="sm"
color="destructive"
className="w-full rounded-none font-medium"
StartIcon={Icon.FiX}
onClick={declineInvite}>
@@ -158,11 +156,10 @@ export default function TeamListItem(props: Props) {
<DropdownMenuContent hidden={hideDropdown}>
{isAdmin && (
<DropdownMenuItem>
<Link href={"/settings/teams/" + team.id}>
<Link href={"/settings/teams/" + team.id + "/profile"}>
<a>
<Button
color="minimal"
size="sm"
className="w-full rounded-none font-medium"
StartIcon={Icon.FiEdit2}>
{t("edit_team")}
@@ -176,7 +173,6 @@ export default function TeamListItem(props: Props) {
<a target="_blank">
<Button
color="minimal"
size="sm"
className="w-full rounded-none font-medium"
StartIcon={Icon.FiExternalLink}>
{t("preview_team")}
@@ -193,8 +189,7 @@ export default function TeamListItem(props: Props) {
onClick={(e) => {
e.stopPropagation();
}}
color="warn"
size="sm"
color="destructive"
className="w-full rounded-none font-medium"
StartIcon={Icon.FiTrash}>
{t("disband_team")}
@@ -220,7 +215,7 @@ export default function TeamListItem(props: Props) {
<DialogTrigger asChild>
<Button
type="button"
color="warn"
color="destructive"
size="lg"
StartIcon={Icon.FiLogOut}
className="w-full rounded-none"
@@ -163,7 +163,7 @@ function ConnectedCalendarsList(props: Props) {
logo={item.integration.logo}
description={item.primary?.externalId || "No external Id"}
actions={
<div className="w-32">
<div className="flex w-32 justify-end">
<DisconnectIntegration
credentialId={item.credentialId}
trashIcon
@@ -41,6 +41,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
const [showEventNameTip, setShowEventNameTip] = useState(false);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const [seatsInputVisible, setSeatsInputVisible] = useState(!!eventType.seatsPerTimeSlot);
const [seatsPerTimeSlot, setSeatsPerTimeSlot] = useState(eventType.seatsPerTimeSlot);
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
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 && (
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
<DialogContent
@@ -27,13 +27,13 @@ const AppCard = ({
}) => {
return (
<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" />
<div className="flex flex-col">
<span className="font-semibold leading-none text-black">{name}</span>
<p className="pt-2 text-sm font-normal text-gray-600">{description}</p>
</div>
<div className="ml-auto">
<div className="ml-auto flex items-center">
<Switch onCheckedChange={switchOnClick} checked={switchChecked} />
</div>
</div>
@@ -6,7 +6,7 @@ function SkeletonLoader() {
<SkeletonContainer>
<div className="mb-4 flex items-center">
<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-24" />
</div>
+1
View File
@@ -17,6 +17,7 @@ const V2_WHITELIST = [
"/event-types",
"/workflows",
"/apps",
"/teams",
"/success",
"/auth/login",
];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.0.0",
"version": "2.0.1",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+1 -1
View File
@@ -115,7 +115,7 @@ const providers: Provider[] = [
};
},
}),
// ImpersonationProvider,
ImpersonationProvider,
];
if (IS_GOOGLE_LOGIN_ENABLED) {
+10 -7
View File
@@ -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
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
users = [await getLuckyUser("MAXIMIZE_AVAILABILITY", { availableUsers, eventTypeId: eventType.id })];
} else {
// excluding ROUND_ROBIN, all users have availability required.
if (availableUsers.length !== users.length) {
throw new Error("Some users are unavailable for booking.");
if (!eventType.seatsPerTimeSlot) {
if (eventType.schedulingType === SchedulingType.ROUND_ROBIN) {
users = [await getLuckyUser("MAXIMIZE_AVAILABILITY", { availableUsers, eventTypeId: eventType.id })];
} else {
// excluding ROUND_ROBIN, all users have availability required.
if (availableUsers.length !== users.length) {
throw new Error("Some users are unavailable for booking.");
}
users = availableUsers;
}
users = availableUsers;
}
console.log("available users", users);
-1
View File
@@ -1,4 +1,3 @@
import UserV2OptInBanner from "@calcom/features/users/components/UserV2OptInBanner";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { MobileNavigationMoreItems } from "@calcom/ui/Shell";
import { Shell } from "@calcom/ui/v2";
@@ -100,7 +100,7 @@ const IntegrationsList = ({ data }: { data: inferQueryOutput<"viewer.integration
logo={item.logo}
description={item.description}
actions={
<div className="flex w-32 justify-center">
<div className="flex w-32 justify-end">
<ConnectOrDisconnectIntegrationButton
credentialIds={item.credentialIds}
type={item.type}
+1 -1
View File
@@ -140,7 +140,7 @@ export default function Login({
{...form.register("email")}
/>
<div className="relative">
<div className="absolute right-0 -top-[6px]">
<div className="absolute right-0 -top-[6px] z-10">
<Link href="/auth/forgot-password">
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
{t("forgot")}
@@ -165,7 +165,7 @@ const AppearanceView = () => {
</>
)}
/>
<Button color="primary" className="mt-8">
<Button type="submit" color="primary" className="mt-8">
{t("update")}
</Button>
</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")}</>
</Button>
</Form>
@@ -4,6 +4,7 @@ import { useRef, useState, BaseSyntheticEvent } from "react";
import { Controller, useForm } from "react-hook-form";
import { ErrorCode } from "@calcom/lib/auth";
import { WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
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 [hasDeleteErrors, setHasDeleteErrors] = useState(false);
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>) => {
setHasDeleteErrors(true);
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>) => {
e.preventDefault();
const totpCode = form.getValues("totpCode");
@@ -134,7 +153,11 @@ const ProfileView = () => {
<Form
form={formMethods}
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" />
<div className="flex items-center">
@@ -159,73 +182,24 @@ const ProfileView = () => {
)}
/>
</div>
<Controller
control={formMethods.control}
name="username"
render={({ field: { value } }) => (
<div className="mt-8">
<TextField
data-testid="username-input"
name="username"
label={t("personal_cal_url")}
addOnLeading="https://cal.com/"
value={value}
onChange={(e) => {
formMethods.setValue("username", e?.target.value);
}}
/>
</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>
)}
/>
<div className="mt-8">
<TextField
data-testid="username-input"
label={t("personal_cal_url")}
addOnLeading={WEBSITE_URL + "/"}
{...formMethods.register("username")}
/>
</div>
<div className="mt-8">
<TextField label={t("full_name")} {...formMethods.register("name")} />
</div>
<div className="mt-8">
<TextField label={t("email")} hint={t("change_email_hint")} {...formMethods.register("email")} />
</div>
<div className="mt-8">
<TextField label={t("about")} hint={t("bio_hint")} {...formMethods.register("bio")} />
</div>
<Button color="primary" className="mt-8" type="submit" loading={mutation.isLoading}>
{t("update")}
</Button>
@@ -279,6 +253,32 @@ const ProfileView = () => {
</DialogContent>
</Dialog>
</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>
</>
);
};
+72
View File
@@ -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.",
"duplicate": "Duplicate",
"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",
"number_of_seats": "Number of seats per booking",
"enter_number_of_seats": "Enter number of seats",
@@ -1265,5 +1265,7 @@
"no_calendar_installed": "No calendar installed",
"no_calendar_installed_description": "You have not yet connected any of your calendars",
"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"
}
@@ -49,7 +49,7 @@ const TextWidget = (props: TextWidgetProps & { type?: string }) => {
const textValue = value || "";
return (
<TextField
containerClassName="w-full"
containerClassName="w-full mt-2"
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"
value={textValue}
@@ -243,8 +243,10 @@ const Route = ({
<TextField
name="externalRedirectUrl"
className="flex w-full flex-grow border-gray-300 text-sm"
containerClassName="w-full mt-2"
type="text"
required
labelSrOnly
value={route.action.value}
onChange={(e) => {
setRoute(route.id, { action: { ...route.action, value: e.target.value } });
@@ -1,10 +1,13 @@
import { User } from "@prisma/client";
import CredentialsProvider from "next-auth/providers/credentials";
import { getSession } from "next-auth/react";
import { z } from "zod";
import prisma from "@calcom/prisma";
import { asNumberOrThrow } from "@lib/asStringOrNull";
const teamIdschema = z.object({
teamId: z.number(),
});
const auditAndReturnNextUser = async (
impersonatedUser: Pick<User, "id" | "username" | "email" | "name" | "role">,
@@ -50,7 +53,8 @@ const ImpersonationProvider = CredentialsProvider({
// 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 });
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) {
throw new Error("You cannot impersonate yourself.");
@@ -102,6 +106,8 @@ const ImpersonationProvider = CredentialsProvider({
return auditAndReturnNextUser(impersonatedUser, session?.user.id as number);
}
if (!teamId) throw new Error("You do not have permission to do this.");
// Check session
const sessionUserFromDb = await prisma.user.findUnique({
where: {
@@ -19,9 +19,6 @@ function WorkflowsPage() {
const session = useSession();
const router = useRouter();
const me = useMeQuery();
const isFreeUser = me.data?.plan === "FREE";
const { data, isLoading } = trpc.useQuery(["viewer.workflows.list"]);
const createMutation = trpc.useMutation("viewer.workflows.createV2", {
@@ -47,7 +44,7 @@ function WorkflowsPage() {
title={t("workflows")}
subtitle={data?.workflows.length ? t("workflows_to_automate_notifications") : ""}
CTA={
session.data?.hasValidLicense && !isFreeUser && data?.workflows && data?.workflows.length > 0 ? (
session.data?.hasValidLicense && data?.workflows && data?.workflows.length > 0 ? (
<Button
StartIcon={Icon.FiPlus}
onClick={() => createMutation.mutate()}
@@ -63,11 +60,7 @@ function WorkflowsPage() {
<SkeletonLoader />
) : (
<>
{isFreeUser ? (
<Alert className="border " severity="warning" title={t("pro_feature_workflows")} />
) : (
<WorkflowList workflows={data?.workflows} />
)}
<WorkflowList workflows={data?.workflows} />
</>
)}
</LicenseRequired>
@@ -69,8 +69,6 @@ function WorkflowPage() {
const { t, i18n } = useLocale();
const session = useSession();
const router = useRouter();
const me = useMeQuery();
const isFreeUser = me.data?.plan === "FREE";
const [selectedEventTypes, setSelectedEventTypes] = useState<Option[]>([]);
const [isAllDataLoaded, setIsAllDataLoaded] = useState(false);
@@ -189,45 +187,36 @@ function WorkflowPage() {
<Shell
title={workflow && workflow.name ? workflow.name : "Untitled"}
CTA={
!isFreeUser && (
<div>
<Button type="submit">{t("save")}</Button>
</div>
)
<div>
<Button type="submit">{t("save")}</Button>
</div>
}
heading={
session.data?.hasValidLicense &&
isAllDataLoaded &&
!isFreeUser && (
isAllDataLoaded && (
<div className={classNames(workflow && !workflow.name ? "text-gray-400" : "")}>
{workflow && workflow.name ? workflow.name : "untitled"}
</div>
)
}>
<LicenseRequired>
{isFreeUser ? (
<Alert className="border " severity="warning" title={t("pro_feature_workflows")} />
) : (
{!isError ? (
<>
{!isError ? (
{isAllDataLoaded ? (
<>
{isAllDataLoaded ? (
<>
<WorkflowDetailsPage
form={form}
workflowId={+workflowId}
selectedEventTypes={selectedEventTypes}
setSelectedEventTypes={setSelectedEventTypes}
/>
</>
) : (
<SkeletonLoader />
)}
<WorkflowDetailsPage
form={form}
workflowId={+workflowId}
selectedEventTypes={selectedEventTypes}
setSelectedEventTypes={setSelectedEventTypes}
/>
</>
) : (
<Alert severity="error" title="Something went wrong" message={error.message} />
<SkeletonLoader />
)}
</>
) : (
<Alert severity="error" title="Something went wrong" message={error.message} />
)}
</LicenseRequired>
</Shell>
@@ -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';
+1 -1
View File
@@ -163,7 +163,7 @@ model User {
identityProviderId String?
availability Availability[]
invitedTo Int?
plan UserPlan @default(FREE)
plan UserPlan @default(PRO)
webhooks Webhook[]
brandColor String @default("#292929")
darkBrandColor String @default("#fafafa")
+69 -44
View File
@@ -8,56 +8,81 @@ import { TRPCError } from "@trpc/server";
import { createProtectedRouter } from "../../createRouter";
export const authRouter = createProtectedRouter().mutation("changePassword", {
input: z.object({
oldPassword: z.string(),
newPassword: z.string(),
}),
async resolve({ input, ctx }) {
const { oldPassword, newPassword } = input;
export const authRouter = createProtectedRouter()
.mutation("changePassword", {
input: z.object({
oldPassword: z.string(),
newPassword: z.string(),
}),
async resolve({ input, ctx }) {
const { oldPassword, newPassword } = input;
const { user } = ctx;
const { user } = ctx;
if (user.identityProvider !== IdentityProvider.CAL) {
throw new TRPCError({ code: "FORBIDDEN", message: "THIRD_PARTY_IDENTITY_PROVIDER_ENABLED" });
}
if (user.identityProvider !== IdentityProvider.CAL) {
throw new TRPCError({ code: "FORBIDDEN", message: "THIRD_PARTY_IDENTITY_PROVIDER_ENABLED" });
}
const currentPasswordQuery = await prisma.user.findFirst({
where: {
id: user.id,
},
select: {
password: true,
},
});
const currentPasswordQuery = await prisma.user.findFirst({
where: {
id: user.id,
},
select: {
password: true,
},
});
const currentPassword = currentPasswordQuery?.password;
const currentPassword = currentPasswordQuery?.password;
if (!currentPassword) {
throw new TRPCError({ code: "NOT_FOUND", message: "MISSING_PASSWORD" });
}
if (!currentPassword) {
throw new TRPCError({ code: "NOT_FOUND", message: "MISSING_PASSWORD" });
}
const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
if (!passwordsMatch) {
throw new TRPCError({ code: "BAD_REQUEST", message: "INCORRECT_PASSWORD" });
}
const passwordsMatch = await verifyPassword(oldPassword, currentPassword);
if (!passwordsMatch) {
throw new TRPCError({ code: "BAD_REQUEST", message: "INCORRECT_PASSWORD" });
}
if (oldPassword === newPassword) {
throw new TRPCError({ code: "BAD_REQUEST", message: "PASSWORD_MATCHES_OLD" });
}
if (oldPassword === newPassword) {
throw new TRPCError({ code: "BAD_REQUEST", message: "PASSWORD_MATCHES_OLD" });
}
if (!validPassword(newPassword)) {
throw new TRPCError({ code: "BAD_REQUEST", message: "INVALID_PASSWORD" });
}
if (!validPassword(newPassword)) {
throw new TRPCError({ code: "BAD_REQUEST", message: "INVALID_PASSWORD" });
}
const hashedPassword = await hashPassword(newPassword);
await prisma.user.update({
where: {
id: user.id,
},
data: {
password: hashedPassword,
},
});
},
});
const hashedPassword = await hashPassword(newPassword);
await prisma.user.update({
where: {
id: user.id,
},
data: {
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;
},
});
+9 -1
View File
@@ -382,6 +382,8 @@ export type NavigationItemType = {
icon?: SVGComponent;
child?: NavigationItemType[];
pro?: true;
onlyMobile?: boolean;
onlyDesktop?: boolean;
isCurrent?: ({
item,
isChild,
@@ -411,6 +413,12 @@ const navigation: NavigationItemType[] = [
href: "/availability",
icon: Icon.FiClock,
},
{
name: "teams",
href: "/teams",
icon: Icon.FiUsers,
onlyDesktop: true,
},
{
name: "apps",
href: "/apps",
@@ -480,7 +488,7 @@ const { desktopNavigationItems, mobileNavigationBottomItems, mobileNavigationMor
// We filter out the "more" separator in desktop navigation
if (item.name !== MORE_SEPARATOR_NAME) items.desktopNavigationItems.push(item);
// 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
else items.mobileNavigationMoreItems.push(item);
return items;
+3 -3
View File
@@ -61,7 +61,7 @@ export default function AllApps({ apps }: AllAppsPropsType) {
return (
<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 ">
{t("explore_apps", {
category:
@@ -70,7 +70,7 @@ export default function AllApps({ apps }: AllAppsPropsType) {
})}
</h2>
{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">
<ChevronLeft className="h-4 w-4 text-gray-500" />
</div>
@@ -112,7 +112,7 @@ export default function AllApps({ apps }: AllAppsPropsType) {
))}
</ul>
{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 items-center justify-end bg-white">
<ChevronRight className="h-4 w-4 text-gray-500" />
+1 -1
View File
@@ -269,7 +269,7 @@ export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(funct
<Tooltip content={textLabel}>
<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"
onClick={() => toggleIsPasswordVisible()}>
{isPasswordVisible ? (
@@ -47,7 +47,7 @@ export default function InstalledAppsLayout({
}
return (
<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">
<VerticalTabs tabs={actualTabs} sticky />
</div>
@@ -213,14 +213,14 @@ const SettingsSidebarContainer = ({ className = "" }) => {
{team.accepted && (
<VerticalTabItem
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"
disableChevron
/>
)}
<VerticalTabItem
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"
disableChevron
/>
@@ -235,7 +235,7 @@ const SettingsSidebarContainer = ({ className = "" }) => {
/> */}
<VerticalTabItem
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"
disableChevron
/>
@@ -153,7 +153,11 @@ export default function CreateEventTypeButton(props: CreateEventTypeBtnProps) {
) : (
<Dropdown>
<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>
<DropdownMenuContent align="end">
<DropdownMenuLabel>