Skip to content

Commit ff6fa64

Browse files
CarinaWolliCarinaWolli
andauthored
fix: UI improvements for team-wide limits (calcom#27133)
* add badge * allow undefined * fix ci type errors --------- Co-authored-by: CarinaWolli <wollencarina@gmail.com>
1 parent ea76df4 commit ff6fa64

3 files changed

Lines changed: 88 additions & 15 deletions

File tree

apps/web/modules/event-types/components/tabs/limits/EventLimitsTab.tsx

Lines changed: 68 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import findDurationType from "@calcom/lib/findDurationType";
1515
import { useLocale } from "@calcom/lib/hooks/useLocale";
1616
import { ascendingLimitKeys, intervalLimitKeyToUnit } from "@calcom/lib/intervalLimits/intervalLimit";
1717
import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
18-
import { PeriodType } from "@calcom/prisma/enums";
18+
import { PeriodType, SchedulingType } from "@calcom/prisma/enums";
1919
import classNames from "@calcom/ui/classNames";
2020
import { Button } from "@calcom/ui/components/button";
2121
import {
@@ -28,6 +28,7 @@ import {
2828
} from "@calcom/ui/components/form";
2929
import { Icon } from "@calcom/ui/components/icon";
3030
import { Tooltip } from "@calcom/ui/components/tooltip";
31+
import { Badge } from "@calcom/ui/components/badge";
3132
import { LearnMoreLink } from "@calcom/web/modules/event-types/components/LearnMoreLink";
3233
import { useAutoAnimate } from "@formkit/auto-animate/react";
3334
import * as RadioGroup from "@radix-ui/react-radio-group";
@@ -36,6 +37,7 @@ import React, { useEffect, useState } from "react";
3637
import type { UseFormRegisterReturn, UseFormReturn } from "react-hook-form";
3738
import { Controller, useFormContext } from "react-hook-form";
3839
import type { SingleValue } from "react-select";
40+
import Link from "next/link";
3941

4042
import MaxActiveBookingsPerBookerController from "./MaxActiveBookingsPerBookerController";
4143

@@ -135,13 +137,13 @@ function RangeLimitRadioItem({
135137
"text-default mb-2 flex flex-col items-start text-sm sm:flex-row sm:items-center",
136138
customClassNames?.wrapper
137139
)}>
138-
<div className="flex w-full items-center sm:w-auto">
140+
<div className="flex items-center w-full sm:w-auto">
139141
{!isDisabled && (
140142
<RadioGroup.Item
141143
id={radioValue}
142144
value={radioValue}
143-
className="bg-default border-default flex h-4 w-4 min-w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
144-
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
145+
className="flex items-center w-4 h-4 rounded-full border cursor-pointer bg-default border-default min-w-4 focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
146+
<RadioGroup.Indicator className="flex relative justify-center items-center w-4 h-4 after:bg-inverted after:block after:h-2 after:w-2 after:rounded-full" />
145147
</RadioGroup.Item>
146148
)}
147149
<span>{t("within_date_range")}</span>
@@ -223,8 +225,8 @@ function RollingLimitRadioItem({
223225
<RadioGroup.Item
224226
id={radioValue}
225227
value={radioValue}
226-
className="bg-default border-default flex h-4 w-4 min-w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
227-
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
228+
className="flex items-center w-4 h-4 rounded-full border cursor-pointer bg-default border-default min-w-4 focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
229+
<RadioGroup.Indicator className="flex relative justify-center items-center w-4 h-4 after:bg-inverted after:block after:h-2 after:w-2 after:rounded-full" />
228230
</RadioGroup.Item>
229231
)}
230232

@@ -259,7 +261,7 @@ function RollingLimitRadioItem({
259261
/>
260262
<span className="me-2 ms-2">&nbsp;{t("into_the_future")}</span>
261263
</div>
262-
<div className="-ml-6 flex flex-col py-2">
264+
<div className="flex flex-col py-2 -ml-6">
263265
<div className="flex items-center">
264266
<CheckboxField
265267
checked={!!rollingExcludeUnavailableDays}
@@ -289,7 +291,7 @@ function RollingLimitRadioItem({
289291
})}>
290292
<Icon
291293
name="info"
292-
className="text-default hover:text-attention hover:bg-attention ms-1 inline h-4 w-4 rounded-md"
294+
className="inline w-4 h-4 rounded-md text-default hover:text-attention hover:bg-attention ms-1"
293295
/>
294296
</Tooltip>
295297
</div>
@@ -350,7 +352,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
350352
}, [minimumBookingNoticeDisplayValues, setValue, passThroughProps.name]);
351353

352354
return (
353-
<div className="flex items-end justify-end">
355+
<div className="flex justify-end items-end">
354356
<div className="w-1/2 md:w-full">
355357
<InputField
356358
required
@@ -399,7 +401,54 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr
399401
const { t, i18n } = useLocale();
400402
const formMethods = useFormContext<FormValues>();
401403

402-
const isRecurringEvent = !!formMethods.getValues("recurringEvent");
404+
405+
const hasTeamLimits = () => {
406+
const team = eventType.team as { bookingLimits?: IntervalLimit | null; includeManagedEventsInLimits?: boolean } | null | undefined;
407+
const parentTeam = (eventType.parent as { team?: { bookingLimits?: IntervalLimit | null; includeManagedEventsInLimits?: boolean } } | null | undefined)?.team;
408+
409+
const teamHasLimits =
410+
!!team?.bookingLimits && Object.keys(team.bookingLimits).length > 0;
411+
412+
const parentTeamHasLimits =
413+
!!parentTeam?.bookingLimits &&
414+
Object.keys(parentTeam.bookingLimits).length > 0;
415+
416+
const includeManaged =
417+
!!parentTeam?.includeManagedEventsInLimits ||
418+
!!team?.includeManagedEventsInLimits;
419+
420+
if (teamHasLimits) {
421+
if(eventType.schedulingType === SchedulingType.MANAGED) return includeManaged;
422+
return true;
423+
}
424+
425+
return parentTeamHasLimits && includeManaged;
426+
};
427+
428+
const TeamLimitsBadge = ({ isManagedChild, teamId }: { isManagedChild: boolean, teamId?: number | null }) => {
429+
const badge = (
430+
<Badge variant="blue" className="text-xs cursor-pointer hover:opacity-80">
431+
{t("team_limits_apply")}
432+
</Badge>
433+
);
434+
435+
if (isManagedChild) {
436+
return <Tooltip content={t("managed_by_team_admins")}>{badge}</Tooltip>;
437+
}
438+
439+
if (teamId) {
440+
return (
441+
<Link
442+
href={`/settings/teams/${teamId}/settings`}
443+
target="_blank"
444+
rel="noopener noreferrer"
445+
>
446+
{badge}
447+
</Link>
448+
);
449+
}
450+
return null;
451+
};
403452

404453
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager({
405454
eventType,
@@ -432,7 +481,7 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr
432481
"border-subtle stack-y-6 rounded-lg border p-6",
433482
customClassNames?.bufferAndNoticeSection?.container
434483
)}>
435-
<div className="stack-y-4 lg:stack-y-0 flex flex-col lg:flex-row lg:space-x-4">
484+
<div className="flex flex-col stack-y-4 lg:stack-y-0 lg:flex-row lg:space-x-4">
436485
<div
437486
className={classNames(
438487
"w-full",
@@ -526,7 +575,7 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr
526575
/>
527576
</div>
528577
</div>
529-
<div className="stack-y-4 lg:stack-y-0 flex flex-col lg:flex-row lg:space-x-4">
578+
<div className="flex flex-col stack-y-4 lg:stack-y-0 lg:flex-row lg:space-x-4">
530579
<div
531580
className={classNames(
532581
"w-full",
@@ -603,6 +652,10 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr
603652
toggleSwitchAtTheEnd={true}
604653
labelClassName={classNames("text-sm", customClassNames?.bookingFrequencyLimit?.label)}
605654
title={t("limit_booking_frequency")}
655+
Badge={
656+
hasTeamLimits()
657+
? TeamLimitsBadge({ isManagedChild: !!eventType.parent, teamId: eventType.team?.id }) : null
658+
}
606659
{...bookingLimitsLocked}
607660
description={
608661
<LearnMoreLink
@@ -703,7 +756,7 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr
703756
onChange({});
704757
}
705758
}}>
706-
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
759+
<div className="p-6 rounded-b-lg border border-t-0 border-subtle">
707760
<IntervalLimitsManager
708761
propertyName="durationLimits"
709762
defaultLimit={60}
@@ -756,7 +809,7 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr
756809
}
757810
return onChange(isEnabled ? PeriodType.ROLLING : PeriodType.UNLIMITED);
758811
}}>
759-
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
812+
<div className="p-6 rounded-b-lg border border-t-0 border-subtle">
760813
<RadioGroup.Root
761814
value={watchPeriodTypeUiValue}
762815
onValueChange={(val) => {
@@ -821,7 +874,7 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr
821874
formMethods.setValue("offsetStart", 0, { shouldDirty: true });
822875
}
823876
}}>
824-
<div className={classNames("border-subtle rounded-b-lg border border-t-0 p-6")}>
877+
<div className={classNames("p-6 rounded-b-lg border border-t-0 border-subtle")}>
825878
<TextField
826879
required
827880
type="number"

apps/web/public/static/locales/en/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3328,6 +3328,8 @@
33283328
"booking_limits": "Booking limits",
33293329
"booking_limits_team_description": "Booking limits for team members across all team event types",
33303330
"limit_team_booking_frequency_description": "Limit how many times members can be booked across all team event types",
3331+
"team_limits_apply": "Team limits apply",
3332+
"managed_by_team_admins": "Managed by team admins",
33313333
"booking_limits_updated_successfully": "Booking limits updated successfully",
33323334
"you_are_unauthorized_to_make_this_change_to_the_booking": "You are unauthorized to make this change to the booking",
33333335
"delegation_credential_disabled": "Delegation credential disabled",

packages/features/eventtypes/repositories/eventTypeRepository.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -637,6 +637,13 @@ export class EventTypeRepository implements IEventTypesRepository {
637637
select: {
638638
id: true,
639639
teamId: true,
640+
team: {
641+
select: {
642+
id: true,
643+
bookingLimits: true,
644+
includeManagedEventsInLimits: true,
645+
},
646+
},
640647
},
641648
},
642649
teamId: true,
@@ -653,6 +660,8 @@ export class EventTypeRepository implements IEventTypesRepository {
653660
slug: true,
654661
parentId: true,
655662
rrTimestampBasis: true,
663+
bookingLimits: true,
664+
includeManagedEventsInLimits: true,
656665
parent: {
657666
select: {
658667
slug: true,
@@ -943,6 +952,13 @@ export class EventTypeRepository implements IEventTypesRepository {
943952
select: {
944953
id: true,
945954
teamId: true,
955+
team: {
956+
select: {
957+
id: true,
958+
bookingLimits: true,
959+
includeManagedEventsInLimits: true,
960+
},
961+
},
946962
},
947963
},
948964
teamId: true,
@@ -959,6 +975,8 @@ export class EventTypeRepository implements IEventTypesRepository {
959975
slug: true,
960976
parentId: true,
961977
rrTimestampBasis: true,
978+
bookingLimits: true,
979+
includeManagedEventsInLimits: true,
962980
parent: {
963981
select: {
964982
slug: true,

0 commit comments

Comments
 (0)