Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8c0e2b5129 | ||
|
|
cf32287109 | ||
|
|
2ef8e230c5 | ||
|
|
546d0d50c4 | ||
|
|
33e8198779 | ||
|
|
e7dd0ff3e9 | ||
|
|
1b2707d4f0 | ||
|
|
00071bcdf0 | ||
|
|
fc5cc5b8bc | ||
|
|
1e79a55825 | ||
|
|
4645bbcb75 | ||
|
|
95b903110f | ||
|
|
2917455324 | ||
|
|
d71765848c | ||
|
|
cf7b7d7d3d | ||
|
|
96aaa36751 | ||
|
|
52e74245a4 | ||
|
|
cc03b132e6 |
@@ -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
Submodule apps/api updated: fcb9ca6ace...a36b6fa923
@@ -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")}
|
||||
|
||||
@@ -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,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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -17,6 +17,7 @@ const V2_WHITELIST = [
|
||||
"/event-types",
|
||||
"/workflows",
|
||||
"/apps",
|
||||
"/teams",
|
||||
"/success",
|
||||
"/auth/login",
|
||||
];
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.0.0",
|
||||
"version": "2.0.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -115,7 +115,7 @@ const providers: Provider[] = [
|
||||
};
|
||||
},
|
||||
}),
|
||||
// ImpersonationProvider,
|
||||
ImpersonationProvider,
|
||||
];
|
||||
|
||||
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
|
||||
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,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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
+1
-1
Submodule apps/website updated: b9d73fd609...0ebd35d880
+1
-1
@@ -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>
|
||||
|
||||
+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?
|
||||
availability Availability[]
|
||||
invitedTo Int?
|
||||
plan UserPlan @default(FREE)
|
||||
plan UserPlan @default(PRO)
|
||||
webhooks Webhook[]
|
||||
brandColor String @default("#292929")
|
||||
darkBrandColor String @default("#fafafa")
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user