Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/mighty-wings-create.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@calcom/atoms": minor
---

feat: style calendar settings and availability overrides
45 changes: 39 additions & 6 deletions docs/platform/atoms/availability-settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,45 @@ Along with the props, Availability settings atom accepts custom styles via the *
| formClassName | Form which contains the days and toggles |
| timezoneSelectClassName | Adds styling to the timezone select component |
| subtitlesClassName | Styles the subtitle |
| scheduleContainer | Styles the entire schedule component |
| scheduleDay | Adds styling to just the day of a particular schedule |
| dayRanges | Adds styling to day ranges |
| timeRanges | Time ranges in the availability settings can be customized |
| labelAndSwitchContainer | Adds styling to label and switches |
| overridesModalClassNames | Adds styling to the date overrides modal |
| scheduleClassNames | An object for granular styling of schedule components (see detailed table below) |
| dateOverrideClassNames | An object for granular styling of date override components (see detailed table below) |

### scheduleClassNames Object Structure

The `scheduleClassNames` prop accepts an object with the following structure for granular styling of schedule components:

| Property Path | Description |
|:--------------|:------------|
| `schedule` | Styles the entire schedule component |
| `scheduleContainer` | Styles the schedule container |
| `scheduleDay` | Adds styling to just the day of a particular schedule |
| `dayRanges` | Adds styling to day ranges |
| `timeRangeField` | Styles the time range input fields |
| `labelAndSwitchContainer` | Adds styling to label and switches |
| `timePicker` | An object for granular styling of time picker dropdown components (see nested table below) |

#### timePicker Object Structure (nested within scheduleClassNames)

The `timePicker` prop accepts an object with the following structure for granular styling of time picker dropdown components. This applies to the time selection dropdowns (e.g., [ 9:00 ]) that users can click to open a dropdown with available times or click to manually enter a time:

| Property Path | Description |
|:--------------|:------------|
| `container` | Styles the main time picker container |
| `valueContainer` | Styles the container that holds the selected value |
| `value` | Styles the displayed selected time value |
| `input` | Styles the input field for manual time entry |
| `dropdown` | Styles the dropdown menu with time options |

### dateOverrideClassNames Object Structure

The `dateOverrideClassNames` prop accepts an object with the following structure for granular styling of date override components:

| Property Path | Description |
|:--------------|:------------|
| `container` | Styles the main container |
| `title` | Styles the title|
| `description` | Styles the description |
| `button` | Styles the button to add date override |

<Info>Please ensure all custom classnames are valid [Tailwind CSS](https://tailwindcss.com/) classnames.</Info>

Expand Down
31 changes: 31 additions & 0 deletions docs/platform/atoms/calendar-settings.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,37 @@ Along with the props, calendar settings atom accepts custom styles via the **cla
| calendarSettingsCustomClassnames | Adds styling to the entire calendar settings atom |
| destinationCalendarSettingsCustomClassnames | Adds styling only to the destination calendar container |
| selectedCalendarSettingsCustomClassnames | Adds styling only to the selected calendar container |
| selectedCalendarSettingsClassNames | An object for granular styling of selected calendars component (see detailed table below) |
| destinationCalendarSettingsClassNames | An object for granular styling of destination calendar component (see detailed table below) |

### selectedCalendarSettingsClassNames Object Structure

The `selectedCalendarSettingsClassNames` prop accepts an object with the following nested structure for granular styling of the selected calendars component:

| Property Path | Description |
|:--------------|:------------|
| `container` | Styles the main container of the selected calendars section |
| `header.container` | Styles the header container |
| `header.title` | Styles the header title |
| `header.description` | Styles the header description |
| `selectedCalendarsListClassNames.container` | Styles the container that holds all selected calendar items |
| `selectedCalendarsListClassNames.selectedCalendar.container` | Styles each individual calendar item container |
| `selectedCalendarsListClassNames.selectedCalendar.header.container` | Styles the header section of each calendar item |
| `selectedCalendarsListClassNames.selectedCalendar.header.title` | Styles the title of each calendar item |
| `selectedCalendarsListClassNames.selectedCalendar.header.description` | Styles the description of each calendar item |
| `selectedCalendarsListClassNames.selectedCalendar.body.container` | Styles the body section of each calendar item |
| `selectedCalendarsListClassNames.selectedCalendar.body.description` | Styles the body description of each calendar item |
| `noSelectedCalendarsMessage` | Styles the message shown when no calendars are connected |

### destinationCalendarSettingsClassNames Object Structure

The `destinationCalendarSettingsClassNames` prop accepts an object with the following nested structure for granular styling of the destination calendar component:

| Property Path | Description |
|:--------------|:------------|
| `container` | Styles the main container of the destination calendar section |
| `header.title` | Styles the header title text |
| `header.description` | Styles the header description text |

<p></p>
Additionally, if you wish to select either the Destination Calendar or the Selected Calendar, we also provide atoms specifically designed for this purpose.
Expand Down
31 changes: 30 additions & 1 deletion packages/features/schedules/components/Schedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ export type ScheduleLabelsType = {
deleteTime: string;
};

export type SelectInnerClassNames = {
control?: string;
singleValue?: string;
valueContainer?: string;
input?: string;
menu?: string;
};

export type FieldPathByValue<TFieldValues extends FieldValues, TValue> = {
[Key in FieldPath<TFieldValues>]: FieldPathValue<TFieldValues, Key> extends TValue ? Key : never;
}[FieldPath<TFieldValues>];
Expand Down Expand Up @@ -105,6 +113,7 @@ export const ScheduleDay = <TFieldValues extends FieldValues>({
classNames={{
dayRanges: classNames?.dayRanges,
timeRangeField: classNames?.timeRangeField,
timePicker: classNames?.timePicker,
}}
/>
{!disabled && <div className="block">{CopyButton}</div>}
Expand Down Expand Up @@ -237,7 +246,7 @@ export const DayRanges = <TFieldValues extends FieldValues>({
disabled?: boolean;
labels?: ScheduleLabelsType;
userTimeFormat: number | null;
classNames?: Pick<scheduleClassNames, "dayRanges" | "timeRangeField">;
classNames?: Pick<scheduleClassNames, "dayRanges" | "timeRangeField" | "timePicker">;
}) => {
const { t } = useLocale();
const { getValues } = useFormContext();
Expand All @@ -260,6 +269,7 @@ export const DayRanges = <TFieldValues extends FieldValues>({
<TimeRangeField
className={classNames?.timeRangeField}
userTimeFormat={userTimeFormat}
timePickerClassNames={classNames?.timePicker}
{...field}
/>
)}
Expand Down Expand Up @@ -335,11 +345,27 @@ const TimeRangeField = ({
onChange,
disabled,
userTimeFormat,
timePickerClassNames,
}: {
className?: string;
disabled?: boolean;
userTimeFormat: number | null;
timePickerClassNames?: {
container?: string;
value?: string;
valueContainer?: string;
input?: string;
dropdown?: string;
};
} & ControllerRenderProps) => {
const innerClassNames: SelectInnerClassNames = {
control: timePickerClassNames?.container,
singleValue: timePickerClassNames?.value,
valueContainer: timePickerClassNames?.valueContainer,
input: timePickerClassNames?.input,
menu: timePickerClassNames?.dropdown,
};

// this is a controlled component anyway given it uses LazySelect, so keep it RHF agnostic.
return (
<div className={cn("flex flex-row gap-2 sm:gap-3", className)}>
Expand All @@ -349,6 +375,7 @@ const TimeRangeField = ({
isDisabled={disabled}
value={value.start}
menuPlacement="bottom"
innerClassNames={innerClassNames}
onChange={(option) => {
const newStart = new Date(option?.value as number);
if (newStart >= new Date(value.end)) {
Expand All @@ -367,6 +394,7 @@ const TimeRangeField = ({
isDisabled={disabled}
value={value.end}
min={value.start}
innerClassNames={innerClassNames}
menuPlacement="bottom"
onChange={(option) => {
onChange({ ...value, end: new Date(option?.value as number) });
Expand All @@ -388,6 +416,7 @@ const LazySelect = ({
min?: ConfigType;
max?: ConfigType;
userTimeFormat: number | null;
innerClassNames?: SelectInnerClassNames;
}) => {
// Lazy-loaded options, otherwise adding a field has a noticeable redraw delay.
const { options, filter } = useOptions(userTimeFormat);
Expand Down
65 changes: 44 additions & 21 deletions packages/platform/atoms/availability/AvailabilitySettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export type CustomClassNames = {
subtitlesClassName?: string;
scheduleClassNames?: scheduleClassNames;
overridesModalClassNames?: string;
dateOverrideClassNames?: {
container?: string;
title?: string;
description?: string;
button?: string;
};
hiddenSwitchClassname?: {
container?: string;
thumb?: string;
Expand Down Expand Up @@ -180,13 +186,20 @@ const DateOverride = ({
travelSchedules,
weekStart,
overridesModalClassNames,
classNames,
handleSubmit,
}: {
workingHours: WorkingHours[];
userTimeFormat: number | null;
travelSchedules?: RouterOutputs["viewer"]["travelSchedules"]["get"];
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6;
overridesModalClassNames?: string;
classNames?: {
container?: string;
title?: string;
description?: string;
button?: string;
};
handleSubmit: (data: AvailabilityFormValues) => Promise<void>;
}) => {
const { append, replace, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
Expand All @@ -202,16 +215,18 @@ const DateOverride = ({
};

return (
<div className="p-6">
<h3 className="text-emphasis font-medium leading-6">
<div className={cn("p-6", classNames?.container)}>
<h3 className={cn("text-emphasis font-medium leading-6", classNames?.title)}>
{t("date_overrides")}{" "}
<Tooltip content={t("date_overrides_info")}>
<span className="inline-block align-middle">
<Icon name="info" className="h-4 w-4" />
</span>
</Tooltip>
</h3>
<p className="text-subtle mb-4 text-sm">{t("date_overrides_subtitle")}</p>
<p className={cn("text-subtle mb-4 text-sm", classNames?.description)}>
{t("date_overrides_subtitle")}
</p>
<div className="space-y-2">
<DateOverrideList
excludedDates={excludedDates}
Expand All @@ -235,7 +250,11 @@ const DateOverride = ({
userTimeFormat={userTimeFormat}
weekStart={weekStart}
Trigger={
<Button color="secondary" StartIcon="plus" data-testid="add-override">
<Button
className={classNames?.button}
color="secondary"
StartIcon="plus"
data-testid="add-override">
{t("add_an_override")}
</Button>
}
Expand Down Expand Up @@ -318,24 +337,27 @@ export const AvailabilitySettings = forwardRef<AvailabilitySettingsFormRef, Avai

const callbacksRef = useRef<{ onSuccess?: () => void; onError?: (error: Error) => void }>({});

const handleFormSubmit = useCallback((customCallbacks?: { onSuccess?: () => void; onError?: (error: Error) => void }) => {
if (customCallbacks) {
callbacksRef.current = customCallbacks;
}
const handleFormSubmit = useCallback(
(customCallbacks?: { onSuccess?: () => void; onError?: (error: Error) => void }) => {
if (customCallbacks) {
callbacksRef.current = customCallbacks;
}

if (saveButtonRef.current) {
saveButtonRef.current.click();
} else {
form.handleSubmit(async (data) => {
try {
await handleSubmit(data);
callbacksRef.current?.onSuccess?.();
} catch (error) {
callbacksRef.current?.onError?.(error as Error);
}
})();
}
}, [form, handleSubmit]);
if (saveButtonRef.current) {
saveButtonRef.current.click();
} else {
form.handleSubmit(async (data) => {
try {
await handleSubmit(data);
callbacksRef.current?.onSuccess?.();
} catch (error) {
callbacksRef.current?.onError?.(error as Error);
}
})();
}
},
[form, handleSubmit]
);

const validateForm = useCallback(async () => {
const isValid = await form.trigger();
Expand Down Expand Up @@ -661,6 +683,7 @@ export const AvailabilitySettings = forwardRef<AvailabilitySettingsFormRef, Avai
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
}
overridesModalClassNames={customClassNames?.overridesModalClassNames}
classNames={customClassNames?.dateOverrideClassNames}
/>
)}
</div>
Expand Down
7 changes: 7 additions & 0 deletions packages/platform/atoms/availability/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ export type scheduleClassNames = {
timeRangeField?: string;
labelAndSwitchContainer?: string;
scheduleContainer?: string;
timePicker?: {
container?: string;
valueContainer?: string;
value?: string;
input?: string;
dropdown?: string;
};
};

export type AvailabilityFormValidationResult = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import type { DestinationCalendarClassNames } from "../../destination-calendar/DestinationCalendar";
import { DestinationCalendarSettingsPlatformWrapper } from "../../destination-calendar/index";
import { SelectedCalendarsSettingsPlatformWrapper } from "../../selected-calendars/index";
import type { CalendarRedirectUrls } from "../../selected-calendars/wrappers/SelectedCalendarsSettingsPlatformWrapper";
import type {
CalendarRedirectUrls,
SelectedCalendarsClassNames,
} from "../../selected-calendars/wrappers/SelectedCalendarsSettingsPlatformWrapper";

type CalendarSettingsPlatformWrapperProps = {
classNames?: {
calendarSettingsCustomClassnames?: string;
destinationCalendarSettingsCustomClassnames?: string;
selectedCalendarSettingsCustomClassnames?: string;
selectedCalendarSettingsClassNames?: SelectedCalendarsClassNames;
destinationCalendarSettingsClassNames?: DestinationCalendarClassNames;
};
calendarRedirectUrls?: CalendarRedirectUrls;
allowDelete?: boolean;
Expand All @@ -24,13 +30,15 @@ export const CalendarSettingsPlatformWrapper = ({
<DestinationCalendarSettingsPlatformWrapper
statusLoader={<></>}
classNames={classNames?.destinationCalendarSettingsCustomClassnames}
classNamesObject={classNames?.destinationCalendarSettingsClassNames}
isDryRun={isDryRun}
/>
<SelectedCalendarsSettingsPlatformWrapper
classNames={classNames?.selectedCalendarSettingsCustomClassnames}
calendarRedirectUrls={calendarRedirectUrls}
allowDelete={allowDelete}
isDryRun={isDryRun}
classNamesObject={classNames?.selectedCalendarSettingsClassNames}
/>
</div>
);
Expand Down
Loading
Loading