feat: implement booking calendar view with weekly layout (#24563)
* feat: implement booking calendar view with weekly layout - Create reusable WeekCalendarView component that displays bookings in a weekly calendar format - Replace EmptyScreen in BookingsCalendar with the new calendar view - Calendar view includes: - Week navigation with Today, Previous, and Next buttons - 7-day week view with time slots from 12 AM to 11 PM - Bookings displayed as colored blocks positioned by time - Support for event type colors and status-based colors - Responsive design that fills the viewport - Hover tooltips showing booking details - Filters remain functional at the top of the view Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * refactor: use existing Calendar component from weeklyview - Replace custom calendar implementation with the existing Calendar component - Use parseEventTypeColor to properly handle event type colors - Simplify implementation by leveraging existing calendar infrastructure - Maintain week navigation and filtering functionality Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * fix imports * fix: replace isSameOrAfter with isAfter || isSame - isSameOrAfter method does not exist in dayjs - Use combination of isAfter and isSame instead Co-Authored-By: eunjae@cal.com <hey@eunjae.dev> * remove useBookerTime dependency from weekly calendar view * modify date range filters * initial callback * sort events * clean up FilterBar * add showBackgroundPattern * update styles * update style * update styles * fix type error * fix error * update styles * update styles * update event colors * rename component * persist weekStart on the url * use FilterBar * apply feedback * extract BorderColor type * use client * clean up * adjust styles * color-code events * rename borderColor to color * restore class name * add feature flag * update class name --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -6,7 +6,9 @@ import { redirect } from "next/navigation";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
import { buildLegacyRequest } from "@lib/buildLegacyCtx";
|
||||
@@ -52,12 +54,18 @@ const Page = async ({ params }: PageProps) => {
|
||||
canReadOthersBookings = teamIdsWithPermission.length > 0;
|
||||
}
|
||||
|
||||
const featuresRepository = new FeaturesRepository(prisma);
|
||||
const isCalendarViewEnabled = await featuresRepository.checkIfFeatureIsEnabledGlobally(
|
||||
"booking-calendar-view"
|
||||
);
|
||||
|
||||
return (
|
||||
<ShellMainAppDir heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||
<BookingsList
|
||||
status={parsed.data.status}
|
||||
userId={session?.user?.id}
|
||||
permissions={{ canReadOthersBookings }}
|
||||
isCalendarViewEnabled={isCalendarViewEnabled}
|
||||
/>
|
||||
</ShellMainAppDir>
|
||||
);
|
||||
|
||||
@@ -1,23 +1,96 @@
|
||||
"use client";
|
||||
|
||||
import type { Table as ReactTable } from "@tanstack/react-table";
|
||||
import { createParser, useQueryState } from "nuqs";
|
||||
import { useMemo, useCallback } from "react";
|
||||
|
||||
import { DataTableFilters, DataTableSegment } from "@calcom/features/data-table";
|
||||
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import {
|
||||
DataTableFilters,
|
||||
DataTableSegment,
|
||||
useDataTable,
|
||||
useFilterValue,
|
||||
ZDateRangeFilterValue,
|
||||
ColumnFilterType,
|
||||
} from "@calcom/features/data-table";
|
||||
import { CUSTOM_PRESET } from "@calcom/features/data-table/lib/dateRange";
|
||||
|
||||
import type { RowData, BookingListingStatus } from "../types";
|
||||
import { BookingsCalendarView } from "./BookingsCalendarView";
|
||||
|
||||
type BookingsCalendarViewProps = {
|
||||
status: BookingListingStatus;
|
||||
table: ReactTable<RowData>;
|
||||
};
|
||||
|
||||
const COLUMN_IDS_TO_HIDE = ["dateRange"];
|
||||
|
||||
const weekStartParser = createParser({
|
||||
parse: (value: string) => {
|
||||
const parsed = dayjs(value);
|
||||
return parsed.isValid() ? parsed.startOf("week") : dayjs().startOf("week");
|
||||
},
|
||||
serialize: (value: dayjs.Dayjs) => value.format("YYYY-MM-DD"),
|
||||
});
|
||||
|
||||
export function BookingsCalendar({ table }: BookingsCalendarViewProps) {
|
||||
const { rows } = table.getRowModel();
|
||||
const { updateFilter } = useDataTable();
|
||||
const dateRange = useFilterValue("dateRange", ZDateRangeFilterValue)?.data;
|
||||
|
||||
const [currentWeekStart, setCurrentWeekStart] = useQueryState(
|
||||
"weekStart",
|
||||
weekStartParser.withDefault(dayjs().startOf("week"))
|
||||
);
|
||||
|
||||
const bookings = useMemo(() => {
|
||||
return rows
|
||||
.filter((row) => row.original.type === "data")
|
||||
.map((row) => (row.original.type === "data" ? row.original.booking : null))
|
||||
.filter((booking): booking is NonNullable<typeof booking> => booking !== null);
|
||||
}, [rows]);
|
||||
|
||||
const handleWeekStartChange = useCallback(
|
||||
(newWeekStart: dayjs.Dayjs) => {
|
||||
setCurrentWeekStart(newWeekStart);
|
||||
|
||||
const startDate = newWeekStart.toDate();
|
||||
const endDate = newWeekStart.add(6, "day").toDate();
|
||||
|
||||
if (!dateRange) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rangeStart = dateRange.startDate ? new Date(dateRange.startDate) : null;
|
||||
const rangeEnd = dateRange.endDate ? new Date(dateRange.endDate) : null;
|
||||
|
||||
const needsStartUpdate = !rangeStart || startDate < rangeStart;
|
||||
const needsEndUpdate = !rangeEnd || endDate > rangeEnd;
|
||||
|
||||
if (!needsStartUpdate && !needsEndUpdate) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newStartDate = needsStartUpdate ? startDate : rangeStart;
|
||||
const newEndDate = needsEndUpdate ? endDate : rangeEnd;
|
||||
|
||||
updateFilter("dateRange", {
|
||||
type: ColumnFilterType.DATE_RANGE,
|
||||
data: {
|
||||
startDate: newStartDate.toISOString(),
|
||||
endDate: newEndDate.toISOString(),
|
||||
preset: CUSTOM_PRESET.value,
|
||||
},
|
||||
});
|
||||
},
|
||||
[dateRange, updateFilter, setCurrentWeekStart]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<DataTableFilters.FilterBar table={table} />
|
||||
<DataTableFilters.FilterBar table={table} columnIdsToHide={COLUMN_IDS_TO_HIDE} />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -26,9 +99,11 @@ export function BookingsCalendar({ table }: BookingsCalendarViewProps) {
|
||||
<DataTableSegment.Select />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-center pt-2 xl:pt-0">
|
||||
<EmptyScreen Icon="calendar" headline="Calendar view" description="Calendar view is coming soon." />
|
||||
</div>
|
||||
<BookingsCalendarView
|
||||
bookings={bookings}
|
||||
currentWeekStart={currentWeekStart}
|
||||
onWeekStartChange={handleWeekStartChange}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,150 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useEffect } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||
import type { CalendarEvent } from "@calcom/features/calendars/weeklyview/types/events";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useGetTheme } from "@calcom/lib/hooks/useTheme";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { ButtonGroup } from "@calcom/ui/components/buttonGroup";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { BookingOutput } from "../types";
|
||||
|
||||
type BookingsCalendarViewProps = {
|
||||
bookings: BookingOutput[];
|
||||
currentWeekStart: dayjs.Dayjs;
|
||||
onWeekStartChange: (weekStart: dayjs.Dayjs) => void;
|
||||
};
|
||||
|
||||
export function BookingsCalendarView({
|
||||
bookings,
|
||||
currentWeekStart,
|
||||
onWeekStartChange,
|
||||
}: BookingsCalendarViewProps) {
|
||||
const { t } = useLocale();
|
||||
const { timezone } = useTimePreferences();
|
||||
const { resolvedTheme, forcedTheme } = useGetTheme();
|
||||
|
||||
const goToPreviousWeek = () => {
|
||||
onWeekStartChange(currentWeekStart.subtract(1, "week"));
|
||||
};
|
||||
|
||||
const goToNextWeek = () => {
|
||||
onWeekStartChange(currentWeekStart.add(1, "week"));
|
||||
};
|
||||
|
||||
const goToToday = () => {
|
||||
onWeekStartChange(dayjs().startOf("week"));
|
||||
};
|
||||
|
||||
const startDate = useMemo(() => currentWeekStart.toDate(), [currentWeekStart]);
|
||||
const endDate = useMemo(() => currentWeekStart.add(6, "day").toDate(), [currentWeekStart]);
|
||||
|
||||
useEffect(() => {
|
||||
onWeekStartChange(currentWeekStart);
|
||||
}, []);
|
||||
|
||||
const events = useMemo<CalendarEvent[]>(() => {
|
||||
const hasDarkTheme = !forcedTheme && resolvedTheme === "dark";
|
||||
|
||||
return bookings
|
||||
.filter((booking) => {
|
||||
const bookingStart = dayjs(booking.startTime);
|
||||
return (
|
||||
(bookingStart.isAfter(currentWeekStart) || bookingStart.isSame(currentWeekStart)) &&
|
||||
bookingStart.isBefore(currentWeekStart.add(7, "day"))
|
||||
);
|
||||
})
|
||||
.sort((a, b) => {
|
||||
const startDiff = new Date(a.startTime).getTime() - new Date(b.startTime).getTime();
|
||||
if (startDiff !== 0) return startDiff;
|
||||
return new Date(a.endTime).getTime() - new Date(b.endTime).getTime();
|
||||
})
|
||||
.map((booking, idx) => {
|
||||
// Parse eventTypeColor and extract the appropriate color based on theme
|
||||
const eventTypeColor =
|
||||
booking.eventType?.eventTypeColor &&
|
||||
booking.eventType.eventTypeColor[hasDarkTheme ? "darkEventTypeColor" : "lightEventTypeColor"];
|
||||
|
||||
return {
|
||||
id: idx,
|
||||
title: booking.title,
|
||||
start: new Date(booking.startTime),
|
||||
end: new Date(booking.endTime),
|
||||
options: {
|
||||
status: booking.status,
|
||||
...(eventTypeColor && { color: eventTypeColor }),
|
||||
},
|
||||
};
|
||||
});
|
||||
}, [bookings, currentWeekStart, resolvedTheme, forcedTheme]);
|
||||
|
||||
const weekStart = currentWeekStart;
|
||||
const weekEnd = currentWeekStart.add(6, "day");
|
||||
const startMonth = weekStart.format("MMM");
|
||||
const endMonth = weekEnd.format("MMM");
|
||||
const year = weekEnd.format("YYYY");
|
||||
|
||||
const weekRange =
|
||||
startMonth === endMonth ? (
|
||||
<>
|
||||
<span className="text-emphasis">{`${startMonth} ${weekStart.format("D")} - ${weekEnd.format(
|
||||
"D"
|
||||
)}`}</span>
|
||||
<span className="text-muted">, {year}</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-emphasis">{`${weekStart.format("MMM D")} - ${weekEnd.format("MMM D")}`}</span>
|
||||
<span className="text-muted">, {year}</span>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="border-subtle flex h-[calc(100vh-260px)] min-h-[600px] flex-col rounded-2xl border">
|
||||
<div className="mx-4 mt-4 flex items-center justify-between py-1.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<h2 className="text-xl font-semibold">{weekRange}</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button color="secondary" onClick={goToToday} className="capitalize leading-4">
|
||||
{t("today")}
|
||||
</Button>
|
||||
<ButtonGroup combined>
|
||||
<Button color="secondary" onClick={goToPreviousWeek}>
|
||||
<span className="sr-only">{t("view_previous_week")}</span>
|
||||
<Icon name="chevron-left" className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button color="secondary" onClick={goToNextWeek}>
|
||||
<span className="sr-only">{t("view_next_week")}</span>
|
||||
<Icon name="chevron-right" className="h-4 w-4" />
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto overflow-x-hidden rounded-2xl">
|
||||
<Calendar
|
||||
timezone={timezone}
|
||||
sortEvents
|
||||
startHour={0}
|
||||
endHour={23}
|
||||
events={events}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
gridCellsPerHour={4}
|
||||
hoverEventDuration={0}
|
||||
showBackgroundPattern={false}
|
||||
showBorder={false}
|
||||
borderColor="subtle"
|
||||
onEventClick={(_event) => {}}
|
||||
hideHeader
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -40,6 +40,7 @@ type BookingsProps = {
|
||||
permissions: {
|
||||
canReadOthersBookings: boolean;
|
||||
};
|
||||
isCalendarViewEnabled: boolean;
|
||||
};
|
||||
|
||||
function useSystemSegments(userId?: number) {
|
||||
@@ -89,8 +90,10 @@ const viewParser = createParser({
|
||||
serialize: (value: "list" | "calendar") => value,
|
||||
});
|
||||
|
||||
function BookingsContent({ status, permissions }: BookingsProps) {
|
||||
const [view] = useQueryState("view", viewParser.withDefault("list"));
|
||||
function BookingsContent({ status, permissions, isCalendarViewEnabled }: BookingsProps) {
|
||||
const [_view] = useQueryState("view", viewParser.withDefault("list"));
|
||||
// Force view to be "list" if calendar view is disabled
|
||||
const view = isCalendarViewEnabled ? _view : "list";
|
||||
const { t } = useLocale();
|
||||
const user = useMeQuery().data;
|
||||
const searchParams = useSearchParams();
|
||||
|
||||
@@ -86,9 +86,7 @@ function InsightsPageContent() {
|
||||
className="flex flex-wrap items-center gap-2"
|
||||
data-testid={`insights-filters-${isAll}-${teamId}-${userId}`}>
|
||||
<OrgTeamsFilter />
|
||||
<DataTableFilters.AddFilterButton table={table} hideWhenFilterApplied />
|
||||
<DataTableFilters.ActiveFilters table={table} />
|
||||
<DataTableFilters.AddFilterButton table={table} variant="sm" showWhenFilterApplied />
|
||||
<DataTableFilters.FilterBar table={table} />
|
||||
<DataTableFilters.ClearFiltersButton exclude={["startTime", "createdAt"]} />
|
||||
<div className="grow" />
|
||||
<Download />
|
||||
|
||||
@@ -3903,6 +3903,8 @@
|
||||
"onboarding_plan_team_badge": "$15/user/mo",
|
||||
"onboarding_plan_organization_badge": "$37/user/mo",
|
||||
"onboarding_plan_organization_description": "Robust scheduling for larger teams looking to have more control, privacy, and security.",
|
||||
"view_previous_week": "View previous week",
|
||||
"view_next_week": "View next week",
|
||||
"invite_teammates": "Invite Teammates",
|
||||
"organization_brand": "Organization Brand",
|
||||
"organization_details": "Organization Details",
|
||||
|
||||
@@ -65,7 +65,6 @@ export const LargeCalendar = ({
|
||||
start: new Date(booking.start),
|
||||
end: new Date(booking.end),
|
||||
options: {
|
||||
borderColor: "black",
|
||||
status: booking.status.toUpperCase() as BookingStatus,
|
||||
"data-test-id": "troubleshooter-busy-event",
|
||||
className: "border-[1.5px]",
|
||||
|
||||
@@ -13,7 +13,7 @@ export const events: CalendarEvent[] = [
|
||||
end: startDate.add(45, "minutes").toDate(),
|
||||
options: {
|
||||
allDay: false,
|
||||
borderColor: "#ff0000",
|
||||
color: "#ff0000",
|
||||
status: "ACCEPTED",
|
||||
},
|
||||
source: "Booking",
|
||||
@@ -37,7 +37,7 @@ export const events: CalendarEvent[] = [
|
||||
source: "Booking",
|
||||
options: {
|
||||
status: "PENDING",
|
||||
borderColor: "#ff0000",
|
||||
color: "#ff0000",
|
||||
allDay: false,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -31,6 +31,9 @@ export function Calendar(props: CalendarComponentProps) {
|
||||
const availableTimeslots = useCalendarStore((state) => state.availableTimeslots);
|
||||
const hideHeader = useCalendarStore((state) => state.hideHeader);
|
||||
const timezone = useCalendarStore((state) => state.timezone);
|
||||
const showBackgroundPattern = useCalendarStore((state) => state.showBackgroundPattern);
|
||||
const showBorder = useCalendarStore((state) => state.showBorder ?? true);
|
||||
const borderColor = useCalendarStore((state) => state.borderColor ?? "default");
|
||||
|
||||
const days = useMemo(() => getDaysBetweenDates(startDate, endDate), [startDate, endDate]);
|
||||
|
||||
@@ -64,23 +67,41 @@ export function Calendar(props: CalendarComponentProps) {
|
||||
<div
|
||||
style={{ width: "165%" }}
|
||||
className="flex h-full max-w-full flex-none flex-col sm:max-w-none md:max-w-full">
|
||||
<DateValues containerNavRef={containerNav} days={days} />
|
||||
<DateValues
|
||||
containerNavRef={containerNav}
|
||||
days={days}
|
||||
showBorder={showBorder}
|
||||
borderColor={borderColor}
|
||||
/>
|
||||
<div className="relative flex flex-auto">
|
||||
<CurrentTime />
|
||||
<div className="bg-default dark:bg-muted ring-muted border-default sticky left-0 z-10 w-14 flex-none border-l border-r ring-1" />
|
||||
<CurrentTime timezone={timezone} />
|
||||
<div
|
||||
className={classNames(
|
||||
"bg-default dark:bg-muted ring-muted sticky left-0 z-10 w-16 flex-none ring-1",
|
||||
showBorder &&
|
||||
(borderColor === "subtle"
|
||||
? "border-subtle border-l border-r"
|
||||
: "border-default border-l border-r")
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
className="grid flex-auto grid-cols-1 grid-rows-1 [--disabled-gradient-background:#F8F9FB] [--disabled-gradient-foreground:#E6E7EB] dark:[--disabled-gradient-background:#262626] dark:[--disabled-gradient-foreground:#393939]"
|
||||
style={{
|
||||
backgroundColor: "var(--disabled-gradient-background)",
|
||||
background:
|
||||
"repeating-linear-gradient(-45deg, var(--disabled-gradient-background), var(--disabled-gradient-background) 2.5px, var(--disabled-gradient-foreground) 2.5px, var(--disabled-gradient-foreground) 5px)",
|
||||
}}>
|
||||
style={
|
||||
showBackgroundPattern === false
|
||||
? undefined
|
||||
: {
|
||||
backgroundColor: "var(--disabled-gradient-background)",
|
||||
background:
|
||||
"repeating-linear-gradient(-45deg, var(--disabled-gradient-background), var(--disabled-gradient-background) 2.5px, var(--disabled-gradient-foreground) 2.5px, var(--disabled-gradient-foreground) 5px)",
|
||||
}
|
||||
}>
|
||||
<HorizontalLines
|
||||
hours={hours}
|
||||
numberOfGridStopsPerCell={usersCellsStopsPerHour}
|
||||
containerOffsetRef={containerOffset}
|
||||
borderColor={borderColor}
|
||||
/>
|
||||
<VerticalLines days={days} />
|
||||
<VerticalLines days={days} borderColor={borderColor} />
|
||||
|
||||
<SchedulerColumns
|
||||
offsetHeight={containerOffset.current?.offsetHeight}
|
||||
@@ -113,6 +134,7 @@ export function Calendar(props: CalendarComponentProps) {
|
||||
{availableTimeslots ? (
|
||||
<AvailableCellsForDay
|
||||
key={days[i].toISOString()}
|
||||
timezone={timezone}
|
||||
day={days[i]}
|
||||
startHour={startHour}
|
||||
availableSlots={availableTimeslots}
|
||||
|
||||
@@ -4,12 +4,16 @@ import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
import type { BorderColor } from "../../types/common";
|
||||
|
||||
type Props = {
|
||||
showBorder: boolean;
|
||||
borderColor: BorderColor;
|
||||
days: dayjs.Dayjs[];
|
||||
containerNavRef: React.RefObject<HTMLDivElement>;
|
||||
};
|
||||
|
||||
export function DateValues({ days, containerNavRef }: Props) {
|
||||
export function DateValues({ showBorder, borderColor, days, containerNavRef }: Props) {
|
||||
const { i18n } = useLocale();
|
||||
const formatDate = (date: dayjs.Dayjs): string => {
|
||||
return new Intl.DateTimeFormat(i18n.language, { weekday: "short" }).format(date.toDate());
|
||||
@@ -17,8 +21,12 @@ export function DateValues({ days, containerNavRef }: Props) {
|
||||
return (
|
||||
<div
|
||||
ref={containerNavRef}
|
||||
className="bg-default dark:bg-muted border-b-subtle rtl:border-r-default sticky top-[var(--calendar-dates-sticky-offset,0px)] z-[80] flex-none border-b border-r sm:pr-8">
|
||||
<div className="text-subtle flex text-sm leading-6 sm:hidden" data-dayslength={days.length}>
|
||||
className={classNames(
|
||||
"bg-default dark:bg-default sticky top-[var(--calendar-dates-sticky-offset,0px)] z-[80] flex-none border-b sm:pr-8",
|
||||
borderColor === "subtle" ? "border-b-subtle" : "border-b-default",
|
||||
showBorder && (borderColor === "subtle" ? "border-r-subtle border-r" : "border-r-default border-r")
|
||||
)}>
|
||||
<div className="text-subtle flex leading-6 sm:hidden" data-dayslength={days.length}>
|
||||
{days.map((day) => {
|
||||
const isToday = dayjs().isSame(day, "day");
|
||||
return (
|
||||
@@ -29,8 +37,8 @@ export function DateValues({ days, containerNavRef }: Props) {
|
||||
{day.format("dd")}{" "}
|
||||
<span
|
||||
className={classNames(
|
||||
"text-emphasis mt-1 flex h-8 w-8 items-center justify-center font-semibold",
|
||||
isToday && "bg-inverted text-inverted rounded-full"
|
||||
"text-emphasis mt-1 flex h-8 w-8 items-center justify-center font-medium",
|
||||
isToday && "bg-inverted text-inverted rounded-sm"
|
||||
)}>
|
||||
{day.format("D")}
|
||||
</span>
|
||||
@@ -38,8 +46,14 @@ export function DateValues({ days, containerNavRef }: Props) {
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="text-subtle -mr-px hidden auto-cols-fr text-sm leading-6 sm:flex ">
|
||||
<div className="border-default col-end-1 w-14 ltr:border-l" />
|
||||
<div className="text-subtle -mr-px hidden auto-cols-fr leading-6 sm:flex">
|
||||
<div
|
||||
className={classNames(
|
||||
"col-end-1 w-16",
|
||||
showBorder &&
|
||||
(borderColor === "subtle" ? "border-l-subtle border-l" : "border-l-default border-l")
|
||||
)}
|
||||
/>
|
||||
{days.map((day) => {
|
||||
const isToday = dayjs().isSame(day, "day");
|
||||
return (
|
||||
@@ -47,14 +61,14 @@ export function DateValues({ days, containerNavRef }: Props) {
|
||||
key={day.toString()}
|
||||
className={classNames(
|
||||
"flex flex-1 items-center justify-center py-3 text-xs font-medium uppercase",
|
||||
isToday && "font-bold"
|
||||
isToday && "text-default"
|
||||
)}>
|
||||
<span>
|
||||
{formatDate(day)}{" "}
|
||||
<span
|
||||
className={classNames(
|
||||
"items-center justify-center p-1",
|
||||
isToday && "bg-brand-default text-brand rounded-full"
|
||||
isToday && "bg-brand-default text-brand rounded-md"
|
||||
)}>
|
||||
{day.format("DD")}
|
||||
</span>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useEffect, useState, useRef } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
|
||||
import { useBookerTime } from "../../../../bookings/Booker/components/hooks/useBookerTime";
|
||||
import { useCalendarStore } from "../../state/store";
|
||||
|
||||
function calculateMinutesFromStart(startHour: number, currentHour: number, currentMinute: number) {
|
||||
@@ -11,7 +11,8 @@ function calculateMinutesFromStart(startHour: number, currentHour: number, curre
|
||||
return currentMinuteOfDay - startMinute;
|
||||
}
|
||||
|
||||
export function CurrentTime() {
|
||||
export function CurrentTime({ timezone }: { timezone: string }) {
|
||||
const { timeFormat } = useTimePreferences();
|
||||
const currentTimeRef = useRef<HTMLDivElement>(null);
|
||||
const [scrolledIntoView, setScrolledIntoView] = useState(false);
|
||||
const [currentTimePos, setCurrentTimePos] = useState<number | null>(null);
|
||||
@@ -19,7 +20,6 @@ export function CurrentTime() {
|
||||
startHour: state.startHour || 0,
|
||||
endHour: state.endHour || 23,
|
||||
}));
|
||||
const { timeFormat, timezone } = useBookerTime();
|
||||
|
||||
useEffect(() => {
|
||||
// Set the container scroll position based on the current time.
|
||||
@@ -39,11 +39,10 @@ export function CurrentTime() {
|
||||
if (!currentTimeRef.current || scrolledIntoView) return;
|
||||
// Within a small timeout so element has time to render.
|
||||
setTimeout(() => {
|
||||
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- Doesn't seem to cause any issue. Put it under condition if needed
|
||||
// Doesn't seem to cause any issue. Put it under condition if needed
|
||||
currentTimeRef?.current?.scrollIntoView({ block: "center" });
|
||||
setScrolledIntoView(true);
|
||||
}, 100);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [startHour, endHour, scrolledIntoView, timezone]);
|
||||
|
||||
return (
|
||||
@@ -55,7 +54,7 @@ export function CurrentTime() {
|
||||
top: `calc(${currentTimePos}*var(--one-minute-height) + var(--calendar-offset-top))`,
|
||||
zIndex: 70,
|
||||
}}>
|
||||
<div className="w-14 pr-2 text-right">{dayjs().tz(timezone).format(timeFormat)}</div>
|
||||
<div className="w-16 pr-2 text-right">{dayjs().tz(timezone).format(timeFormat)}</div>
|
||||
<div className="bg-inverted h-3 w-px" />
|
||||
<div className="bg-inverted h-px w-screen" />
|
||||
</div>
|
||||
|
||||
@@ -3,10 +3,10 @@ import { shallow } from "zustand/shallow";
|
||||
|
||||
import type { Dayjs } from "@calcom/dayjs";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
import { OutOfOfficeInSlots } from "../../../../bookings/Booker/components/OutOfOfficeInSlots";
|
||||
import { useBookerTime } from "../../../../bookings/Booker/components/hooks/useBookerTime";
|
||||
import { useCalendarStore } from "../../state/store";
|
||||
import type { CalendarAvailableTimeslots } from "../../types/state";
|
||||
import type { GridCellToDateProps } from "../../utils";
|
||||
@@ -33,13 +33,13 @@ export function EmptyCell(props: EmptyCellProps) {
|
||||
}
|
||||
|
||||
type AvailableCellProps = {
|
||||
timezone: string;
|
||||
availableSlots: CalendarAvailableTimeslots;
|
||||
day: GridCellToDateProps["day"];
|
||||
startHour: GridCellToDateProps["startHour"];
|
||||
};
|
||||
|
||||
export function AvailableCellsForDay({ availableSlots, day, startHour }: AvailableCellProps) {
|
||||
const { timezone } = useBookerTime();
|
||||
export function AvailableCellsForDay({ timezone, availableSlots, day, startHour }: AvailableCellProps) {
|
||||
const date = dayjs(day);
|
||||
const dateFormatted = date.format("YYYY-MM-DD");
|
||||
const slotsForToday = availableSlots && availableSlots[dateFormatted];
|
||||
@@ -136,7 +136,7 @@ type CellProps = {
|
||||
};
|
||||
|
||||
function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) {
|
||||
const { timeFormat } = useBookerTime();
|
||||
const { timeFormat } = useTimePreferences();
|
||||
|
||||
const { onEmptyCellClick, hoverEventDuration } = useCalendarStore(
|
||||
(state) => ({
|
||||
@@ -163,7 +163,7 @@ function Cell({ isDisabled, topOffsetMinutes, timeSlot }: CellProps) {
|
||||
top: topOffsetMinutes ? `calc(${topOffsetMinutes}*var(--one-minute-height))` : undefined,
|
||||
}}
|
||||
onClick={() => {
|
||||
onEmptyCellClick && onEmptyCellClick(timeSlot.toDate());
|
||||
onEmptyCellClick?.(timeSlot.toDate());
|
||||
}}>
|
||||
{!isDisabled && hoverEventDuration !== 0 && (
|
||||
<div
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
import { Tooltip } from "@calcom/ui/components/tooltip";
|
||||
|
||||
import type { CalendarEvent } from "../../types/events";
|
||||
|
||||
@@ -15,15 +15,15 @@ type EventProps = {
|
||||
};
|
||||
|
||||
const eventClasses = cva(
|
||||
"group flex h-full w-full overflow-y-auto rounded-[6px] px-[6px] text-xs font-semibold leading-5 opacity-80",
|
||||
"group flex h-full w-full overflow-x-hidden overflow-y-auto rounded-[6px] px-[6px] text-xs leading-5 opacity-80 border-default font-medium",
|
||||
{
|
||||
variants: {
|
||||
status: {
|
||||
ACCEPTED: "bg-subtle hover:bg-emphasis text-emphasis border-[1px] border-gray-900",
|
||||
PENDING: "bg-default text-emphasis border-[1px] border-dashed border-gray-900",
|
||||
REJECTED: "",
|
||||
CANCELLED: "",
|
||||
AWAITING_HOST: "",
|
||||
ACCEPTED: "bg-subtle hover:bg-emphasis text-emphasis border-[1px] border-default",
|
||||
PENDING: "bg-muted text-emphasis border-[1px] border-dashed border-default",
|
||||
REJECTED: "bg-muted border-[1px] text-subtle line-through",
|
||||
CANCELLED: "bg-muted border-[1px] text-subtle line-through",
|
||||
AWAITING_HOST: "bg-muted text-emphasis border-[1px] border-dashed border-default",
|
||||
},
|
||||
disabled: {
|
||||
true: "hover:cursor-default",
|
||||
@@ -33,14 +33,6 @@ const eventClasses = cva(
|
||||
true: "bg-inverted text-inverted border-[1px] border-transparent",
|
||||
false: "",
|
||||
},
|
||||
borderColor: {
|
||||
ACCEPTED: "border-gray-900",
|
||||
PENDING: "border-gray-900",
|
||||
REJECTED: "border-gray-900",
|
||||
CANCELLED: "border-gray-900",
|
||||
AWAITING_HOST: "",
|
||||
custom: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
@@ -55,14 +47,6 @@ export function Event({
|
||||
const selected = currentlySelectedEventId === event.id;
|
||||
const { options } = event;
|
||||
|
||||
const borderColor = options?.borderColor ? "custom" : options?.status;
|
||||
|
||||
const styles = options?.borderColor
|
||||
? {
|
||||
borderColor: options?.borderColor,
|
||||
}
|
||||
: {};
|
||||
|
||||
const Component = onEventClick ? "button" : "div";
|
||||
|
||||
return (
|
||||
@@ -74,32 +58,36 @@ export function Event({
|
||||
status: options?.status,
|
||||
disabled,
|
||||
selected,
|
||||
borderColor,
|
||||
}),
|
||||
eventDuration > 30 && "flex-col py-1",
|
||||
options?.className
|
||||
)}>
|
||||
{options?.color && (
|
||||
<div
|
||||
className="-ml-1.5 mr-1.5 h-full w-[3px] shrink-0"
|
||||
style={{ backgroundColor: options.color }}></div>
|
||||
)}
|
||||
style={styles}>
|
||||
<div
|
||||
className={classNames(
|
||||
"flex w-full gap-2 overflow-hidden overflow-ellipsis whitespace-nowrap text-left leading-4",
|
||||
eventDuration <= 30 && "items-center"
|
||||
)}>
|
||||
<span>{event.title}</span>
|
||||
{eventDuration <= 30 && !event.options?.hideTime && (
|
||||
<p className="text-subtle w-full whitespace-nowrap text-left text-[10px] leading-none">
|
||||
<div className={classNames("flex", eventDuration > 30 && "flex-col py-1")}>
|
||||
<div
|
||||
className={classNames(
|
||||
"flex w-full gap-2 overflow-hidden overflow-ellipsis whitespace-nowrap text-left leading-4",
|
||||
eventDuration <= 30 && "items-center"
|
||||
)}>
|
||||
<span>{event.title}</span>
|
||||
{eventDuration <= 30 && !event.options?.hideTime && (
|
||||
<p className="text-subtle mt-1 w-full whitespace-nowrap text-left text-[10px] leading-none">
|
||||
{dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{eventDuration > 30 && !event.options?.hideTime && (
|
||||
<p className="text-subtle mt-1 text-left text-[10px] leading-none">
|
||||
{dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")}
|
||||
</p>
|
||||
)}
|
||||
{eventDuration > 45 && event.description && (
|
||||
<p className="text-subtle mt-1 text-left text-[10px] leading-none">{event.description}</p>
|
||||
)}
|
||||
</div>
|
||||
{eventDuration > 30 && !event.options?.hideTime && (
|
||||
<p className="text-subtle text-left text-[10px] leading-none">
|
||||
{dayjs(event.start).format("HH:mm")} - {dayjs(event.end).format("HH:mm")}
|
||||
</p>
|
||||
)}
|
||||
{eventDuration > 45 && event.description && (
|
||||
<p className="text-subtle text-left text-[10px] leading-none">{event.description}</p>
|
||||
)}
|
||||
</Component>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -2,15 +2,19 @@ import { useId } from "react";
|
||||
|
||||
import type dayjs from "@calcom/dayjs";
|
||||
import { useTimePreferences } from "@calcom/features/bookings/lib";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
import type { BorderColor } from "../../types/common";
|
||||
|
||||
export const HorizontalLines = ({
|
||||
hours,
|
||||
numberOfGridStopsPerCell,
|
||||
containerOffsetRef,
|
||||
borderColor,
|
||||
}: {
|
||||
hours: dayjs.Dayjs[];
|
||||
numberOfGridStopsPerCell: number;
|
||||
containerOffsetRef: React.RefObject<HTMLDivElement>;
|
||||
borderColor: BorderColor;
|
||||
}) => {
|
||||
const { timeFormat } = useTimePreferences();
|
||||
// We need to force the minute to zero, because otherwise in ex GMT+5.5, it would show :30 minute times (but at the positino of :00)
|
||||
@@ -19,7 +23,10 @@ export const HorizontalLines = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className=" divide-default pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid divide-y"
|
||||
className={classNames(
|
||||
"pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid divide-y",
|
||||
borderColor === "subtle" ? "divide-subtle" : "divide-default"
|
||||
)}
|
||||
style={{
|
||||
gridTemplateRows: `repeat(${hours.length}, minmax(var(--gridDefaultSize),1fr)`,
|
||||
}}>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import type dayjs from "@calcom/dayjs";
|
||||
import classNames from "@calcom/ui/classNames";
|
||||
|
||||
export const VerticalLines = ({ days }: { days: dayjs.Dayjs[] }) => {
|
||||
import type { BorderColor } from "../../types/common";
|
||||
|
||||
export const VerticalLines = ({ days, borderColor }: { days: dayjs.Dayjs[]; borderColor: BorderColor }) => {
|
||||
const isRTL = () => {
|
||||
let userLanguage = "en"; // Default to 'en' if navigator is not defined
|
||||
|
||||
@@ -15,8 +18,10 @@ export const VerticalLines = ({ days }: { days: dayjs.Dayjs[] }) => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className="divide-default pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid
|
||||
auto-cols-auto grid-rows-1 divide-x sm:pr-8"
|
||||
className={classNames(
|
||||
"pointer-events-none relative z-[60] col-start-1 col-end-2 row-start-1 grid auto-cols-auto grid-rows-1 divide-x sm:pr-8",
|
||||
borderColor === "subtle" ? "divide-subtle" : "divide-default"
|
||||
)}
|
||||
dir={direction}
|
||||
style={{
|
||||
direction: direction,
|
||||
|
||||
@@ -20,6 +20,9 @@ const defaultState: CalendarComponentProps = {
|
||||
endHour: 23,
|
||||
gridCellsPerHour: 4,
|
||||
timezone: CURRENT_TIMEZONE,
|
||||
showBackgroundPattern: true,
|
||||
showBorder: true,
|
||||
borderColor: "default",
|
||||
};
|
||||
|
||||
export const useCalendarStore = create<CalendarStoreProps>((set) => ({
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export type BorderColor = "default" | "subtle";
|
||||
@@ -11,7 +11,7 @@ export interface CalendarEvent {
|
||||
status?: BookingStatus;
|
||||
hideTime?: boolean;
|
||||
allDay?: boolean;
|
||||
borderColor?: string;
|
||||
color?: string;
|
||||
className?: string;
|
||||
"data-test-id"?: string;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { IFromUser, IToUser } from "@calcom/features/availability/lib/getUserAvailability";
|
||||
import type { TimeRange } from "@calcom/types/schedule";
|
||||
|
||||
import type { BorderColor } from "./common";
|
||||
import type { CalendarEvent } from "./events";
|
||||
|
||||
export type View = "month" | "week" | "day";
|
||||
@@ -131,6 +132,21 @@ export type CalendarState = {
|
||||
* Timezone to use for displaying times in the calendar
|
||||
*/
|
||||
timezone: string;
|
||||
/**
|
||||
* Show the background pattern for unavailable areas
|
||||
* @default true
|
||||
*/
|
||||
showBackgroundPattern?: boolean;
|
||||
/**
|
||||
* Show borders on the leftmost and rightmost sides of the calendar container
|
||||
* @default true
|
||||
*/
|
||||
showBorder?: boolean;
|
||||
/**
|
||||
* Border color for calendar cells
|
||||
* @default "default"
|
||||
*/
|
||||
borderColor?: BorderColor;
|
||||
};
|
||||
|
||||
export type CalendarComponentProps = CalendarPublicActions & CalendarState & { isPending?: boolean };
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { type Table } from "@tanstack/react-table";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Fragment } from "react";
|
||||
|
||||
import { useDataTable, useFilterableColumns } from "../../hooks";
|
||||
import { ColumnFilterType } from "../../lib/types";
|
||||
@@ -12,9 +10,10 @@ import { FilterPopover } from "./FilterPopover";
|
||||
// Add the new ActiveFilters component
|
||||
interface ActiveFiltersProps<TData> {
|
||||
table: Table<TData>;
|
||||
columnIdsToHide?: string[];
|
||||
}
|
||||
|
||||
export function ActiveFilters<TData>({ table }: ActiveFiltersProps<TData>) {
|
||||
export function ActiveFilters<TData>({ table, columnIdsToHide }: ActiveFiltersProps<TData>) {
|
||||
const { activeFilters } = useDataTable();
|
||||
const filterableColumns = useFilterableColumns(table);
|
||||
|
||||
@@ -22,7 +21,12 @@ export function ActiveFilters<TData>({ table }: ActiveFiltersProps<TData>) {
|
||||
<>
|
||||
{activeFilters.map((filter) => {
|
||||
const column = filterableColumns.find((col) => col.id === filter.f);
|
||||
if (!column) return null;
|
||||
if (!column) {
|
||||
return null;
|
||||
}
|
||||
if (columnIdsToHide?.includes(column.id)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (column.type === ColumnFilterType.DATE_RANGE) {
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { type Table } from "@tanstack/react-table";
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import startCase from "lodash/startCase";
|
||||
import { forwardRef, useState } from "react";
|
||||
|
||||
@@ -17,17 +16,10 @@ import { useDataTable, useFilterableColumns } from "../../hooks";
|
||||
export interface AddFilterButtonProps<TData> {
|
||||
table: Table<TData>;
|
||||
variant?: "base" | "sm";
|
||||
hideWhenFilterApplied?: boolean;
|
||||
showWhenFilterApplied?: boolean;
|
||||
}
|
||||
|
||||
function AddFilterButtonComponent<TData>(
|
||||
{
|
||||
table,
|
||||
variant = "base",
|
||||
hideWhenFilterApplied = false,
|
||||
showWhenFilterApplied = false,
|
||||
}: AddFilterButtonProps<TData>,
|
||||
{ table, variant = "base" }: AddFilterButtonProps<TData>,
|
||||
ref: React.Ref<HTMLButtonElement>
|
||||
) {
|
||||
const { t } = useLocale();
|
||||
@@ -39,14 +31,6 @@ function AddFilterButtonComponent<TData>(
|
||||
(column) => !activeFilters?.some((filter) => filter.f === column.id)
|
||||
);
|
||||
|
||||
if (hideWhenFilterApplied && activeFilters?.length > 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (showWhenFilterApplied && activeFilters?.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (variant === "sm" && availableColumns.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import { type Table } from "@tanstack/react-table";
|
||||
"use client";
|
||||
|
||||
import { type Table } from "@tanstack/react-table";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { useDataTable } from "../../hooks";
|
||||
import { ActiveFilters } from "./ActiveFilters";
|
||||
import { AddFilterButton } from "./AddFilterButton";
|
||||
|
||||
interface FilterBarProps<TData> {
|
||||
table: Table<TData>;
|
||||
columnIdsToHide?: string[];
|
||||
}
|
||||
|
||||
export function FilterBar<TData>({ table }: FilterBarProps<TData>) {
|
||||
export function FilterBar<TData>({ table, columnIdsToHide }: FilterBarProps<TData>) {
|
||||
const { activeFilters } = useDataTable();
|
||||
const displayedFilterCount = useMemo(() => {
|
||||
return (activeFilters ?? []).filter((filter) =>
|
||||
columnIdsToHide ? !columnIdsToHide.includes(filter.f) : true
|
||||
).length;
|
||||
}, [activeFilters, columnIdsToHide]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddFilterButton table={table} hideWhenFilterApplied />
|
||||
<ActiveFilters table={table} />
|
||||
<AddFilterButton table={table} variant="sm" showWhenFilterApplied />
|
||||
{displayedFilterCount === 0 && <AddFilterButton table={table} />}
|
||||
<ActiveFilters table={table} columnIdsToHide={columnIdsToHide} />
|
||||
{displayedFilterCount > 0 && <AddFilterButton table={table} variant="sm" />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ export type AppFlags = {
|
||||
"calendar-subscription-sync": boolean;
|
||||
"onboarding-v3": boolean;
|
||||
"booker-botid": boolean;
|
||||
"booking-calendar-view": boolean;
|
||||
};
|
||||
|
||||
export type TeamFeatures = Record<keyof AppFlags, boolean>;
|
||||
|
||||
@@ -29,6 +29,7 @@ const initialData: AppFlags = {
|
||||
"calendar-subscription-sync": false,
|
||||
"onboarding-v3": false,
|
||||
"booker-botid": false,
|
||||
"booking-calendar-view": false,
|
||||
};
|
||||
|
||||
if (process.env.NEXT_PUBLIC_IS_E2E) {
|
||||
|
||||
@@ -68,8 +68,8 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
|
||||
start: new Date(event.start),
|
||||
end: new Date(event.end),
|
||||
options: {
|
||||
borderColor:
|
||||
event.source && calendarToColorMap[event.source] ? calendarToColorMap[event.source] : "black",
|
||||
color:
|
||||
event.source && calendarToColorMap[event.source] ? calendarToColorMap[event.source] : undefined,
|
||||
status: BookingStatus.ACCEPTED,
|
||||
"data-test-id": "troubleshooter-busy-event",
|
||||
},
|
||||
@@ -99,7 +99,7 @@ export const LargeCalendar = ({ extraDays }: { extraDays: number }) => {
|
||||
start: dateOverrideStart.add(workingHoursForDay.startTime, "minutes").toDate(),
|
||||
end: dateOverrideEnd.add(workingHoursForDay.endTime, "minutes").toDate(),
|
||||
options: {
|
||||
borderColor: "black",
|
||||
color: "black",
|
||||
status: BookingStatus.ACCEPTED,
|
||||
"data-test-id": "troubleshooter-busy-time",
|
||||
},
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
INSERT INTO
|
||||
"Feature" (slug, enabled, description, "type")
|
||||
VALUES
|
||||
(
|
||||
'booking-calendar-view',
|
||||
false,
|
||||
'Enable booking calendar view for all users',
|
||||
'EXPERIMENT'
|
||||
) ON CONFLICT (slug) DO NOTHING;
|
||||
Reference in New Issue
Block a user